
urldsl.language.QueryParameters.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of url-dsl_sjs1_2.12 Show documentation
Show all versions of url-dsl_sjs1_2.12 Show documentation
A tiny library for parsing and creating urls in a type-safe way
The newest version!
package urldsl.language
import app.tulz.tuplez.Composition
import urldsl.errors.{DummyError, ParamMatchingError, SimpleParamMatchingError}
import urldsl.url.{UrlStringDecoder, UrlStringGenerator, UrlStringParserGenerator}
import urldsl.vocabulary._
trait QueryParameters[Q, A] extends UrlPart[Q, A] {
import QueryParameters._
/** Tries to match the map of [[urldsl.vocabulary.Param]]s to create an instance of `Q`. If it can not, it returns an
* error indicating the reason of the failure. If it could, it returns the value of `Q`, as well as the list of
* unused parameters.
*
* @example
* For example, if you try to match a param "name" as String and "age" as Int, calling matchParams on Map("name" ->
* Param(List("Alice")), "age" -> Param(List("24"), "year" -> Param(List("2020"))) will return
* Right(ParamMatchOutput(("Alice", 24), Map("year" -> Param(List("2020")))
*
* @param params
* The map of [[urldsl.vocabulary.Param]] to match this path segment again.
* @return
* The "de-serialized" element with unused parameters, if successful.
*/
def matchParams(params: Map[String, Param]): Either[A, ParamMatchOutput[Q]]
def matchRawUrl(
url: String,
urlStringParserGenerator: UrlStringParserGenerator = UrlStringParserGenerator.defaultUrlStringParserGenerator
): Either[A, Q] =
matchParams(urlStringParserGenerator.parser(url).params).map(_.output)
def matchQueryString(queryString: String, decoder: UrlStringDecoder = UrlStringDecoder.defaultDecoder): Either[A, Q] =
matchParams(decoder.decodeParams(queryString)).map(_.output)
/** Generate a map of parameters representing the argument `q`.
*
* `matchParams` and `createParams` should be (functional) inverse of each other. That is,
* `this.matchParams(this.createParams(q)) == Right(ParamMathOutput(q, Map()))` (this property is called
* "LeftInverse" in the tests)
*/
def createParams(q: Q): Map[String, Param]
/** Generates a Map of parameters representing the argument `q`. The keys are not encoded, but the values are lists of
* encoded strings.
*/
final def createParamsMap(q: Q, encoder: UrlStringGenerator = UrlStringGenerator.default): Map[String, List[String]] =
encoder.makeParamsMap(createParams(q))
/** Generates the query string representing the argument `q`. This String can be used to be part of a URL.
*/
final def createParamsString(q: Q, encoder: UrlStringGenerator = UrlStringGenerator.default): String =
encoder.makeParams(createParams(q))
final def createPart(q: Q, encoder: UrlStringGenerator = UrlStringGenerator.default): String =
createParamsString(q, encoder)
/** Adds `that` QueryParameters to `this` one, "tupling" the returned type with the implicit [[Composition]]
*
* The matching and writing of strings is functionally commutative under `&`, but the returned type `Q` is not. So,
* if you have two parameters, one matching an Int and the other one a String, depending on the order in which `&` is
* called, you can end up with "Q = (Int, String)" or "Q = (String, Int)". This property is called
* "QuasiCommutativity" in the tests.
*/
final def &[R](that: QueryParameters[R, A])(implicit
c: Composition[Q, R]
): QueryParameters[c.Composed, A] =
factory[c.Composed, A](
(params: Map[String, Param]) =>
for {
firstMatch <- this.matchParams(params)
ParamMatchOutput(q, remainingParams) = firstMatch
secondMatch <- that.matchParams(remainingParams)
ParamMatchOutput(r, finalRemainingParams) = secondMatch
} yield ParamMatchOutput(c.compose(q, r), finalRemainingParams),
(out: c.Composed) => {
val (q, r) = c.decompose(out)
this.createParams(q) ++ that.createParams(r)
}
)
/** When these query parameters return an error, transform it to None instead.
*
* This should be used to represent (possibly) missing parameters.
*/
final def ? : QueryParameters[Option[Q], A] = factory[Option[Q], A](
(params: Map[String, Param]) =>
matchParams(params) match {
case Right(ParamMatchOutput(output, unused)) => Right(ParamMatchOutput(Some(output), unused))
case Left(_) => Right(ParamMatchOutput(None, params))
},
_.map(createParams).getOrElse(Map())
)
/** Adds an extra satisfying criteria to the output of this [[QueryParameters]]. If the output satisfies the given
* `predicate`, then it is left unchanged. Otherwise, it returns the given `error`.
*
* Note that it doesn't check that arguments given to `createParams` satisfy this predicate // todo[behaviour]:
* should that change?
*
* @example
* {{{param[Int]("age").filter(_ >= 0, (params: Map[String, Param]) => someError(params))}}}
*
* @param predicate
* the additional predicate that the output must satisfy
* @param error
* the generated error in case it does not satisfy it
* @return
* a new [[QueryParameters]] instance with the same types
*/
final def filter(predicate: Q => Boolean, error: Map[String, Param] => A): QueryParameters[Q, A] = factory(
(params: Map[String, Param]) =>
matchParams(params).filterOrElse(((_: ParamMatchOutput[Q]).output).andThen(predicate), error(params)),
createParams
)
/** Sugar for when `A =:= DummyError`. */
final def filter(predicate: Q => Boolean)(implicit ev: A <:< DummyError): QueryParameters[Q, DummyError] = {
// type F[+E] = QueryParameters[Q, E]
// ev.liftCo[F].apply(this).filter(predicate, _ => DummyError.dummyError)
// we keep this ugliness below while supportinig scala 2.12 todo[scala3] remove
this.asInstanceOf[QueryParameters[Q, DummyError]].filter(predicate, _ => DummyError.dummyError)
}
/** Casts this [[QueryParameters]] to the new type R. Note that the [[urldsl.vocabulary.Codec]] must be an
* exception-free bijection between Q and R.
*/
final def as[R](implicit codec: Codec[Q, R]): QueryParameters[R, A] = factory(
(matchParams _).andThen(_.map(_.map(codec.leftToRight))),
(codec.rightToLeft _).andThen(createParams)
)
}
object QueryParameters {
def factory[Q, A](
matching: Map[String, Param] => Either[A, ParamMatchOutput[Q]],
creating: Q => Map[String, Param]
): QueryParameters[Q, A] = new QueryParameters[Q, A] {
def matchParams(params: Map[String, Param]): Either[A, ParamMatchOutput[Q]] = matching(params)
def createParams(q: Q): Map[String, Param] = creating(q)
}
final def empty[A]: QueryParameters[Unit, A] = factory[Unit, A](
(params: Map[String, Param]) => Right(ParamMatchOutput((), params)),
_ => Map()
)
/** Alias for empty which seems to better reflect the semantic. */
final def ignore[A]: QueryParameters[Unit, A] = empty
final def simpleQueryParam[Q, A](
paramName: String,
matching: Param => Either[A, Q],
creating: Q => Param,
onParameterNotFound: Map[String, Param] => Either[A, ParamMatchOutput[Q]]
)(implicit paramMatchingError: ParamMatchingError[A]): QueryParameters[Q, A] = factory[Q, A](
(params: Map[String, Param]) =>
params
.get(paramName)
.map(matching)
.map(_.map(ParamMatchOutput(_, params - paramName))) // consumes that param
.getOrElse(onParameterNotFound(params)),
creating.andThen(paramName -> _).andThen(Map(_))
)
final def simpleQueryParam[Q, A](
paramName: String,
matching: Param => Either[A, Q],
creating: Q => Param
)(implicit paramMatchingError: ParamMatchingError[A]): QueryParameters[Q, A] =
simpleQueryParam(
paramName,
matching,
creating,
onParameterNotFound = _ => Left(paramMatchingError.missingParameterError(paramName))
)
final def param[Q, A](
paramName: String
)(implicit
fromString: FromString[Q, A],
printer: Printer[Q],
paramMatchingError: ParamMatchingError[A]
): QueryParameters[Q, A] =
simpleQueryParam[Q, A](
paramName,
(_: Param) match {
case Param(Nil) => Left(paramMatchingError.missingParameterError(paramName))
case Param(head :: _) => fromString(head)
},
(q: Q) => Param(List(printer(q)))
)
final def listParam[Q, A](
paramName: String
)(implicit
fromString: FromString[Q, A],
printer: Printer[Q],
paramMatchingError: ParamMatchingError[A]
): QueryParameters[List[Q], A] =
simpleQueryParam[List[Q], A](
paramName,
(_: Param) match {
case Param(Nil) => Right(Nil)
case Param(head :: tail) =>
tail
.map(fromString.apply)
.foldLeft(fromString(head).map(_ :: Nil)) { (acc, next) =>
for {
firstResults <- acc
nextResult <- next
} yield nextResult +: firstResults
}
.map(_.reverse)
},
(q: List[Q]) => Param(q.map(printer.apply)),
// If `paramName` is not present in the parameters we should return an empty list.
onParameterNotFound = (params: Map[String, Param]) => Right(ParamMatchOutput(List.empty[Q], params))
)
final lazy val dummyErrorImpl = QueryParametersImpl[DummyError]
final lazy val simpleParamErrorImpl = QueryParametersImpl[SimpleParamMatchingError]
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy