diff --git a/core/jvm/src/test/scala/krop/route/ParamSuite.scala b/core/jvm/src/test/scala/krop/route/ParamSuite.scala index e1ea114..ffa9662 100644 --- a/core/jvm/src/test/scala/krop/route/ParamSuite.scala +++ b/core/jvm/src/test/scala/krop/route/ParamSuite.scala @@ -25,13 +25,13 @@ class ParamSuite extends FunSuite { using munit.Location ) = values.foreach { case (str, a) => - assertEquals(param.parse(str), Success(a)) + assertEquals(param.parse(str), Right(a)) } def paramOneParsesInvalid[A](param: Param.One[A], values: Seq[String])(using munit.Location ) = - values.foreach { (str) => assert(param.parse(str).isFailure) } + values.foreach { (str) => assert(param.parse(str).isLeft) } test("Param.one parses valid parameter") { paramOneParsesValid( diff --git a/core/jvm/src/test/scala/krop/route/QueryParamSuite.scala b/core/jvm/src/test/scala/krop/route/QueryParamSuite.scala index 5bfaa21..3c4afa8 100644 --- a/core/jvm/src/test/scala/krop/route/QueryParamSuite.scala +++ b/core/jvm/src/test/scala/krop/route/QueryParamSuite.scala @@ -18,19 +18,16 @@ package krop.route import munit.FunSuite -import scala.util.Failure -import scala.util.Success - class QueryParamSuite extends FunSuite { test("Required QueryParam succeeds if first value parses") { val qp = QueryParam("id", Param.int) assertEquals( qp.parse(Map("id" -> List("1"), "name" -> List("Van Gogh"))), - Success(1) + Right(1) ) assertEquals( qp.parse(Map("id" -> List("1", "foobar"), "name" -> List("Van Gogh"))), - Success(1) + Right(1) ) } @@ -38,7 +35,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam("id", Param.int) assertEquals( qp.parse(Map("id" -> List("abc"))), - Failure(QueryParseException.ValueParsingFailed("id", "abc", Param.int)) + Left(QueryParseFailure.ValueParsingFailed("id", "abc", Param.int)) ) } @@ -46,7 +43,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam("id", Param.int) assertEquals( qp.parse(Map("id" -> List())), - Failure(QueryParseException.NoValuesForName("id")) + Left(QueryParseFailure.NoValuesForName("id")) ) } @@ -54,7 +51,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam("id", Param.int) assertEquals( qp.parse(Map("foo" -> List("1"))), - Failure(QueryParseException.NoParameterWithName("id")) + Left(QueryParseFailure.NoParameterWithName("id")) ) } @@ -62,11 +59,11 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam.optional("id", Param.int) assertEquals( qp.parse(Map("id" -> List("1"), "name" -> List("Van Gogh"))), - Success(Some(1)) + Right(Some(1)) ) assertEquals( qp.parse(Map("id" -> List("1", "foobar"), "name" -> List("Van Gogh"))), - Success(Some(1)) + Right(Some(1)) ) } @@ -74,7 +71,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam.optional("id", Param.int) assertEquals( qp.parse(Map("id" -> List("abc"))), - Failure(QueryParseException.ValueParsingFailed("id", "abc", Param.int)) + Left(QueryParseFailure.ValueParsingFailed("id", "abc", Param.int)) ) } @@ -82,7 +79,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam.optional("id", Param.int) assertEquals( qp.parse(Map("id" -> List())), - Success(None) + Right(None) ) } @@ -90,7 +87,7 @@ class QueryParamSuite extends FunSuite { val qp = QueryParam.optional("id", Param.int) assertEquals( qp.parse(Map("foo" -> List("1"))), - Success(None) + Right(None) ) } } diff --git a/core/shared/src/main/scala/krop/route/Param.scala b/core/shared/src/main/scala/krop/route/Param.scala index 307488c..f8fe6c1 100644 --- a/core/shared/src/main/scala/krop/route/Param.scala +++ b/core/shared/src/main/scala/krop/route/Param.scala @@ -20,8 +20,6 @@ import cats.syntax.all.* import java.util.regex.Pattern import scala.collection.immutable.ArraySeq -import scala.util.Success -import scala.util.Try /** A [[package.Param]] is used to extract values from a URI's path or query * parameters. @@ -77,7 +75,7 @@ object Param { */ final case class All[A]( name: String, - parse: Seq[String] => Try[A], + parse: Seq[String] => Either[ParamParseFailure, A], unparse: A => Seq[String] ) extends Param[A] { @@ -100,7 +98,7 @@ object Param { */ final case class One[A]( name: String, - parse: String => Try[A], + parse: String => Either[ParamParseFailure, A], unparse: A => String ) extends Param[A] { @@ -113,16 +111,20 @@ object Param { /** A `Param` that matches a single `Int` parameter */ val int: Param.One[Int] = - Param.One("", str => Try(str.toInt), _.toString) + Param.One( + "", + str => str.toIntOption.toRight(ParamParseFailure(str, "")), + _.toString + ) /** A `Param` that matches a single `String` parameter */ val string: Param.One[String] = - Param.One("", Success(_), identity) + Param.One("", Right(_), identity) /** `Param` that simply accumulates all parameters as a `Seq[String]`. */ val seq: Param.All[Seq[String]] = - Param.All("", Success(_), identity) + Param.All("", Right(_), identity) /** `Param` that matches all parameters and converts them to a `String` by * adding `separator` between matched elements. The inverse splits on this @@ -132,7 +134,7 @@ object Param { val quotedSeparator = Pattern.quote(separator) Param.All( s"${separator}", - seq => Success(seq.mkString(separator)), + seq => Right(seq.mkString(separator)), string => ArraySeq.unsafeWrapArray(string.split(quotedSeparator)) ) } diff --git a/core/shared/src/main/scala/krop/route/ParamParseFailure.scala b/core/shared/src/main/scala/krop/route/ParamParseFailure.scala new file mode 100644 index 0000000..c5a698a --- /dev/null +++ b/core/shared/src/main/scala/krop/route/ParamParseFailure.scala @@ -0,0 +1,19 @@ +/* + * 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 + +final case class ParamParseFailure(value: String, description: String) diff --git a/core/shared/src/main/scala/krop/route/Path.scala b/core/shared/src/main/scala/krop/route/Path.scala index f864cc8..8b28bea 100644 --- a/core/shared/src/main/scala/krop/route/Path.scala +++ b/core/shared/src/main/scala/krop/route/Path.scala @@ -26,8 +26,6 @@ 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 /** A [[krop.route.Path]] represents a pattern to match against the path * component of the URI of a request.`Paths` are created by calling the `/` @@ -222,8 +220,8 @@ final class Path[P <: Tuple, Q <: Tuple] private ( if pathSegments.isEmpty then None else parse(pathSegments(0).decoded()) match { - case Failure(_) => None - case Success(value) => + case Left(_) => None + case Right(value) => loop(matchSegments.tail, pathSegments.tail) match { case None => None case Some(tail) => Some(value *: tail) @@ -232,8 +230,8 @@ final class Path[P <: Tuple, Q <: Tuple] private ( case Param.All(_, parse, _) => parse(pathSegments.map(_.decoded())) match { - case Failure(_) => None - case Success(value) => Some(value *: EmptyTuple) + case Left(_) => None + case Right(value) => Some(value *: EmptyTuple) } } } diff --git a/core/shared/src/main/scala/krop/route/Query.scala b/core/shared/src/main/scala/krop/route/Query.scala index 29361cf..da1f698 100644 --- a/core/shared/src/main/scala/krop/route/Query.scala +++ b/core/shared/src/main/scala/krop/route/Query.scala @@ -18,8 +18,6 @@ package krop.route import cats.syntax.all.* -import scala.util.Try - final case class Query[A <: Tuple](segments: Vector[QueryParam[?]]) { // // Combinators --------------------------------------------------------------- @@ -38,7 +36,7 @@ final case class Query[A <: Tuple](segments: Vector[QueryParam[?]]) { // Interpreters -------------------------------------------------------------- // - def parse(params: Map[String, List[String]]): Try[A] = + def parse(params: Map[String, List[String]]): Either[QueryParseFailure, A] = segments .traverse(s => s.parse(params)) .map(v => Tuple.fromArray(v.toArray).asInstanceOf[A]) diff --git a/core/shared/src/main/scala/krop/route/QueryParam.scala b/core/shared/src/main/scala/krop/route/QueryParam.scala index dbcdf12..7893ce8 100644 --- a/core/shared/src/main/scala/krop/route/QueryParam.scala +++ b/core/shared/src/main/scala/krop/route/QueryParam.scala @@ -16,12 +16,9 @@ package krop.route +import cats.syntax.all.* import krop.route.Param.One -import scala.util.Failure -import scala.util.Success -import scala.util.Try - /** A [[package.QueryParam]] extracts values from a URI's query parameters. It * consists of a [[package.Param]], which does the necessary type conversion, * and the name under which the parameters should be found. @@ -37,7 +34,7 @@ import scala.util.Try * * the `QueryParam` that returns all the query parameters. */ enum QueryParam[A] { - import QueryParseException.* + import QueryParseFailure.* /** Get a human-readable description of this `QueryParam`. */ def describe: String = @@ -47,50 +44,50 @@ enum QueryParam[A] { case All => "all" } - def parse(params: Map[String, List[String]]): Try[A] = + def parse(params: Map[String, List[String]]): Either[QueryParseFailure, A] = this match { case Required(name, param) => params.get(name) match { case Some(values) => param match { - case Param.All(_, parse, _) => parse(values) + case Param.All(_, parse, _) => + parse(values).left.map(f => + ValueParsingFailed(name, f.value, param) + ) case Param.One(_, parse, _) => - if values.isEmpty then Failure(NoValuesForName(name)) + if values.isEmpty then NoValuesForName(name).asLeft else { val hd = values.head - parse(hd).recoverWith(_ => - Failure( - QueryParseException.ValueParsingFailed(name, hd, param) - ) - ) + parse(hd).left.map(_ => ValueParsingFailed(name, hd, param)) } } - case None => Failure(NoParameterWithName(name)) + case None => NoParameterWithName(name).asLeft } case Optional(name, param) => params.get(name) match { case Some(values) => param match { - case Param.All(_, parse, _) => parse(values).map(Some(_)) + case Param.All(_, parse, _) => + parse(values) + .map(Some(_)) + .left + .map(f => ValueParsingFailed(name, f.value, param)) case Param.One(_, parse, _) => - if values.isEmpty then Success(None) + if values.isEmpty then None.asRight else { val hd = values.head parse(hd) .map(Some(_)) - .recoverWith(_ => - Failure( - QueryParseException.ValueParsingFailed(name, hd, param) - ) - ) + .left + .map(_ => ValueParsingFailed(name, hd, param)) } } - case None => Success(None) + case None => None.asRight } - case All => Success(params) + case All => params.asRight } def unparse(a: A): Option[(String, List[String])] = diff --git a/core/shared/src/main/scala/krop/route/QueryParseException.scala b/core/shared/src/main/scala/krop/route/QueryParseFailure.scala similarity index 85% rename from core/shared/src/main/scala/krop/route/QueryParseException.scala rename to core/shared/src/main/scala/krop/route/QueryParseFailure.scala index 83fc19e..651cd6e 100644 --- a/core/shared/src/main/scala/krop/route/QueryParseException.scala +++ b/core/shared/src/main/scala/krop/route/QueryParseFailure.scala @@ -16,14 +16,14 @@ package krop.route -/** Exception raised when query parsing fails. */ -enum QueryParseException(message: String) extends Exception(message) { +/** Failure raised when query parsing fails. */ +enum QueryParseFailure(val message: String) { /** Query parameter parsing failed because no parameter with the given name * was found in the query parameters. */ case NoParameterWithName(name: String) - extends QueryParseException( + extends QueryParseFailure( s"There was no query parameter with the name ${name}." ) @@ -32,12 +32,12 @@ enum QueryParseException(message: String) extends Exception(message) { * with any values. */ case NoValuesForName(name: String) - extends QueryParseException( + extends QueryParseFailure( s"There were no values associated with the name ${name}" ) case ValueParsingFailed(name: String, value: String, param: Param[?]) - extends QueryParseException( + extends QueryParseFailure( s"Parsing the value ${value} as ${param.describe} failed for the query parameter ${name}" ) }