From 45321ea1659ecc626bda75db63141db86670d63f Mon Sep 17 00:00:00 2001 From: Noel Welsh Date: Thu, 12 Mar 2026 14:46:01 +0000 Subject: [PATCH] Refactor Param into Param and Params Replace the awkward Param.One / Param.All representation with separate types for a single parameter and all parameters. Update tests and docs accordingly. Closes #28 --- .../main/scala/krop/asset/AssetRoute.scala | 4 +- core/jvm/src/main/scala/krop/all.scala | 1 + .../test/scala/krop/route/ParamSuite.scala | 40 +++---- .../scala/krop/route/PathParseSuite.scala | 2 +- .../scala/krop/route/PathUnparseSuite.scala | 2 +- .../src/main/scala/krop/route/Param.scala | 111 +++--------------- .../src/main/scala/krop/route/Params.scala | 76 ++++++++++++ .../src/main/scala/krop/route/Path.scala | 59 +++++----- .../main/scala/krop/tool/DefaultAssets.scala | 2 +- .../src/main/scala/krop/tool/KropAssets.scala | 4 +- docs/src/pages/controller/route/paths.md | 35 +++--- examples/src/main/scala/TurboStream.scala | 2 +- 12 files changed, 171 insertions(+), 167 deletions(-) create mode 100644 core/shared/src/main/scala/krop/route/Params.scala diff --git a/asset/src/main/scala/krop/asset/AssetRoute.scala b/asset/src/main/scala/krop/asset/AssetRoute.scala index c98bfa4..d0baf99 100644 --- a/asset/src/main/scala/krop/asset/AssetRoute.scala +++ b/asset/src/main/scala/krop/asset/AssetRoute.scala @@ -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 @@ -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) ) diff --git a/core/jvm/src/main/scala/krop/all.scala b/core/jvm/src/main/scala/krop/all.scala index cad6eee..d690c37 100644 --- a/core/jvm/src/main/scala/krop/all.scala +++ b/core/jvm/src/main/scala/krop/all.scala @@ -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 diff --git a/core/jvm/src/test/scala/krop/route/ParamSuite.scala b/core/jvm/src/test/scala/krop/route/ParamSuite.scala index 2b02d30..dafa781 100644 --- a/core/jvm/src/test/scala/krop/route/ParamSuite.scala +++ b/core/jvm/src/test/scala/krop/route/ParamSuite.scala @@ -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"), @@ -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"), diff --git a/core/jvm/src/test/scala/krop/route/PathParseSuite.scala b/core/jvm/src/test/scala/krop/route/PathParseSuite.scala index 473b66c..17699a3 100644 --- a/core/jvm/src/test/scala/krop/route/PathParseSuite.scala +++ b/core/jvm/src/test/scala/krop/route/PathParseSuite.scala @@ -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("") / "view" val simpleQueryPath = Path / "user" / Param.int :? Query[String]("mode") val multipleQueryPath = diff --git a/core/jvm/src/test/scala/krop/route/PathUnparseSuite.scala b/core/jvm/src/test/scala/krop/route/PathUnparseSuite.scala index e0d17bb..e5940a5 100644 --- a/core/jvm/src/test/scala/krop/route/PathUnparseSuite.scala +++ b/core/jvm/src/test/scala/krop/route/PathUnparseSuite.scala @@ -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("") / "view" val pathWithQuery = Path / "user" / "view" :? Query[Int]("id") diff --git a/core/shared/src/main/scala/krop/route/Param.scala b/core/shared/src/main/scala/krop/route/Param.scala index 6aaa86b..75dd249 100644 --- a/core/shared/src/main/scala/krop/route/Param.scala +++ b/core/shared/src/main/scala/krop/route/Param.scala @@ -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. @@ -50,85 +40,20 @@ sealed abstract class Param[A] extends Product, Serializable { * Param.int.withName("") * ``` */ - 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 "" or "". - * @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 "" or "". - * @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) } diff --git a/core/shared/src/main/scala/krop/route/Params.scala b/core/shared/src/main/scala/krop/route/Params.scala new file mode 100644 index 0000000..aaba3e2 --- /dev/null +++ b/core/shared/src/main/scala/krop/route/Params.scala @@ -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)) +} diff --git a/core/shared/src/main/scala/krop/route/Path.scala b/core/shared/src/main/scala/krop/route/Path.scala index a30ea3f..770b3b1 100644 --- a/core/shared/src/main/scala/krop/route/Path.scala +++ b/core/shared/src/main/scala/krop/route/Path.scala @@ -18,8 +18,6 @@ package krop.route import krop.Types import krop.raise.Raise -import krop.route.Param.All -import krop.route.Param.One import org.http4s.Uri import org.http4s.Uri.Path as UriPath @@ -57,20 +55,19 @@ import scala.collection.mutable * * will match `/assets/example.css` and `/assets/css/example.css`. * - * To capture all segments to the end of the URI's path, use an instance of - * `Param.All` such as `Param.vector`. So + * To capture all segments to the end of the URI's path, use [[Params]]. So * * ``` - * Path.root / "assets" / Param.vector + * Path.root / "assets" / Params.seq * ``` * - * will capture the remainder of the URI's path as a `Vector[String]`. + * will capture the remainder of the URI's path as a `Seq[String]`. * * A `Path` that matches all segments is called a closed path. Attempting to * add an element to a closed path will result in an exception. */ final class Path[P <: Tuple, Q <: Tuple] private ( - val segments: Vector[Segment | Param[?]], + val segments: Vector[Segment | Param[?] | Params[?]], // The number of segments that are of type Param and hence the length of the // Tuple P val paramCount: Int, @@ -98,15 +95,16 @@ final class Path[P <: Tuple, Q <: Tuple] private ( } } - /** Add a segment that extracts a parameter to this `Path`. */ + /** Add a segment that extracts a single parameter to this `Path`. */ def /[B](param: Param[B]): Path[Tuple.Append[P, B], Q] = { assertOpen() - param match { - case Param.One(_) => - Path(segments :+ param, paramCount + 1, query, true) - case Param.All(_) => - Path(segments :+ param, paramCount + 1, query, false) - } + Path(segments :+ param, paramCount + 1, query, true) + } + + /** Add a segment that extracts all remaining parameters to this `Path`. */ + def /[B](params: Params[B]): Path[Tuple.Append[P, B], Q] = { + assertOpen() + Path(segments :+ params, paramCount + 1, query, false) } def :?[B <: Tuple](query: Query[B]): Path[P, B] = @@ -147,7 +145,7 @@ final class Path[P <: Tuple, Q <: Tuple] private ( @tailrec def loop( idx: Int, - segments: Vector[Segment | Param[?]], + segments: Vector[Segment | Param[?] | Params[?]], builder: mutable.StringBuilder ): String = { if segments.isEmpty then builder.result() @@ -159,12 +157,12 @@ final class Path[P <: Tuple, Q <: Tuple] private ( case Segment.All => builder.addOne('/').result() case Segment.One(value) => loop(idx, tl, builder.addOne('/').append(value)) - case p: Param.All[a] => + case p: Params[a] => builder .addOne('/') .append(p.encode(paramsArray(idx).asInstanceOf[a]).mkString("/")) .result() - case p: Param.One[a] => + case p: Param[a] => loop( idx + 1, tl, @@ -199,7 +197,7 @@ final class Path[P <: Tuple, Q <: Tuple] private ( uri: Uri )(using raise: Raise[ParseFailure]): Types.TupleConcat[P, Q] = { def loop( - matchSegments: Vector[Segment | Param[?]], + matchSegments: Vector[Segment | Param[?] | Params[?]], pathSegments: Vector[UriPath.Segment] ): Tuple = if matchSegments.isEmpty then { @@ -221,7 +219,7 @@ final class Path[P <: Tuple, Q <: Tuple] private ( case Segment.All => EmptyTuple - case p: Param.One[a] => + case p: Param[a] => if pathSegments.isEmpty then Path.failure.raise(Path.failure.noMorePathSegments) else @@ -232,7 +230,7 @@ final class Path[P <: Tuple, Q <: Tuple] private ( value *: loop(matchSegments.tail, pathSegments.tail) } - case p: Param.All[a] => + case p: Params[a] => p.decode(pathSegments.map(_.decoded())) match { case Left(err) => Path.failure.raise(Path.failure.paramMismatch(err)) @@ -272,7 +270,7 @@ final class Path[P <: Tuple, Q <: Tuple] private ( @tailrec def loop( idx: Int, - segments: Vector[Segment | Param[?]], + segments: Vector[Segment | Param[?] | Params[?]], path: Uri.Path ): Uri.Path = { if segments.isEmpty then path @@ -284,11 +282,11 @@ final class Path[P <: Tuple, Q <: Tuple] private ( case Segment.All => path.addEndsWithSlash case Segment.One(value) => loop(idx, tl, path.addSegment(value)) - case p: Param.All[a] => + case p: Params[a] => path.addSegments( p.encode(pArr(idx).asInstanceOf[a]).map(Uri.Path.Segment.apply) ) - case p: Param.One[a] => + case p: Param[a] => loop( idx + 1, tl, @@ -308,8 +306,9 @@ final class Path[P <: Tuple, Q <: Tuple] private ( def describe: String = { val p = segments .map { - case s: Segment => s.describe - case p: Param[?] => p.name + case s: Segment => s.describe + case p: Param[?] => p.name + case p: Params[?] => p.name } .mkString("/", "/", "") @@ -335,12 +334,14 @@ object Path { def /(segment: Segment): Path[EmptyTuple, EmptyTuple] = root / segment - /** Create a `Path` that matches the given segment and extracts it as a - * parameter. - */ + /** Create a `Path` that matches a segment and extracts it as a parameter. */ def /[A](param: Param[A]): Path[Tuple1[A], EmptyTuple] = root / param + /** Create a `Path` that extracts all remaining segments as a parameter. */ + def /[A](params: Params[A]): Path[Tuple1[A], EmptyTuple] = + root / params + /** This contains detailed descriptions of why a Path can fail, and utilites * to construct a `ParseFailure` instances and raise them. */ @@ -355,7 +356,7 @@ object Path { """The URI this Path was matching against still contains segments. However |this Path does not match any more segments. To match and ignore all the |remaining segments use Segment.all. The match and capture all remaining - |segments use Param.seq or another variant that captures all + |segments use Params.seq or another variant that captures all |segments.""".stripMargin ) diff --git a/core/shared/src/main/scala/krop/tool/DefaultAssets.scala b/core/shared/src/main/scala/krop/tool/DefaultAssets.scala index 6387bde..1cf090b 100644 --- a/core/shared/src/main/scala/krop/tool/DefaultAssets.scala +++ b/core/shared/src/main/scala/krop/tool/DefaultAssets.scala @@ -18,7 +18,7 @@ package krop.tool import fs2.io.file.Path as Fs2Path import krop.route.Handler -import krop.route.Param.fs2Path +import krop.route.Params.fs2Path import krop.route.Path import krop.route.Request import krop.route.Response diff --git a/core/shared/src/main/scala/krop/tool/KropAssets.scala b/core/shared/src/main/scala/krop/tool/KropAssets.scala index acade44..56fb8fd 100644 --- a/core/shared/src/main/scala/krop/tool/KropAssets.scala +++ b/core/shared/src/main/scala/krop/tool/KropAssets.scala @@ -17,7 +17,7 @@ package krop.tool import krop.route.Handler -import krop.route.Param +import krop.route.Params import krop.route.Path import krop.route.Request import krop.route.Response @@ -26,7 +26,7 @@ import krop.route.Route object KropAssets { val kropAssets: Handler = Route( - Request.get(Path / "krop" / "assets" / Param.separatedString("/")), + Request.get(Path / "krop" / "assets" / Params.separatedString("/")), Response.staticResource("/krop/assets/") ).passthrough } diff --git a/docs/src/pages/controller/route/paths.md b/docs/src/pages/controller/route/paths.md index a4777b2..ae74d92 100644 --- a/docs/src/pages/controller/route/paths.md +++ b/docs/src/pages/controller/route/paths.md @@ -41,23 +41,22 @@ Path / "assets" / Segment.all will match `/assets/`, `/assets/example.css`, and `/assets/css/example.css`. -To capture all segments to the end of the URI's path, use an instance of -`Param.All` such as `Param.seq`. So +To capture all segments to the end of the URI's path, use a @:api(krop.route.Params) instance such as `Params.seq`. So ```scala mdoc:silent -Path / "assets" / Param.seq +Path / "assets" / Params.seq ``` will capture the remainder of the URI's path as a `Seq[String]`. -The `Param.fs2Path` instance is possibly the most useful `Param` capturing all segments. -As the name suggests it converts the captured segments in a `fs2.io.file.Path`, +The `Params.fs2Path` instance is possibly the most useful `Params` capturing all segments. +As the name suggests it converts the captured segments in a `fs2.io.file.Path`, which can then be used to find a file on the file system. The `fs2.io.file.Path` returned is a *relative* path. For example: ```scala mdoc:silent -Path / "assets" / Param.fs2Path +Path / "assets" / Params.fs2Path ``` In use, we see the output is indeed a relative path. @@ -65,7 +64,7 @@ In use, we see the output is indeed a relative path. ```scala mdoc import org.http4s.implicits.* -val path = Path / "assets" / Param.fs2Path +val path = Path / "assets" / Params.fs2Path path.parseToOption(uri"http://example.org/assets/a/b/c.txt") ``` @@ -165,11 +164,13 @@ optional.decode(Map()) ``` -## Params +## Param and Params +A `Param` extracts a single parameter from a path, while a `Params` extract multiple. There are a small number of predefined `Param` instances on the -@:api(krop.route.Param$) companion object. Constructing your own instances can -be done in several ways, described below. +@:api(krop.route.Param$) companion object, and predefined `Params` instances on +the @:api(krop.route.Params$) companion object. Constructing your own instances +can be done in several ways, described below. The `imap` method transforms a `Param[A]` into a `Param[B]` by providing functions `A => B` and `B => A`. This example constructs a `Param[Int]` from the @@ -182,34 +183,34 @@ val intParam = Param.string.imap(_.toInt)(_.toString) intParam.decode("100") ``` -A `Param.One[A]` can be lifted to a `Param.All[Seq[A]]` that uses the given -`Param.One` for every element in the `Seq`. +A `Param[A]` can be lifted to a `Params[Seq[A]]` that uses the given +`Param` for every element in the `Seq`. ```scala mdoc:silent -val intParams = Param.all[Int] +val intParams = Params.all[Int] ``` ```scala mdoc intParams.encode(Seq(1, 2, 3)) ``` -The `separatedString` method can be used for a `Param.All` that constructs a `String` +The `separatedString` method constructs a `Params[String]` containing elements separated by a separator. For example, to accumulate a sub-path we could use the following. ```scala mdoc:silent -val subPath = Param.separatedString("/") +val subPath = Params.separatedString("/") ``` ```scala mdoc subPath.decode(Vector("assets", "css")) subPath.encode("assets/css") ``` -Finally, you can directly call the constructors for `Param.One` and `Param.All`. +Finally, you can directly call the `Param` and `Params` constructors. ### Param Names -`Params` have a `String` name. This is, by convention, some indication of the type written within angle brackets. For example `""` for a `Param[String]`. +`Param` and `Params` have a `String` name. This is, by convention, some indication of the type written within angle brackets. For example `""` for a `Param[String]`. ```scala mdoc Param.string.name diff --git a/examples/src/main/scala/TurboStream.scala b/examples/src/main/scala/TurboStream.scala index 9e57cfe..a58918d 100644 --- a/examples/src/main/scala/TurboStream.scala +++ b/examples/src/main/scala/TurboStream.scala @@ -50,7 +50,7 @@ object TurboStream extends IOApp { val assetRoute = Route( - Request.get(Path.root / "asset" / Param.separatedString("/")), + Request.get(Path.root / "asset" / Params.separatedString("/")), Response.staticResource("/asset/") )