
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_3.0.0-RC3 Show documentation
Show all versions of url-dsl_3.0.0-RC3 Show documentation
A tiny library for parsing and creating urls in a type-safe way
The newest version!
package urldsl.language
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 [[urldsl.language.Tupler]]
*
* 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, A1 >: A](that: QueryParameters[R, A1])(implicit ev: Tupler[Q, R]): QueryParameters[ev.Out, A1] =
factory[ev.Out, A1](
(params: Map[String, Param]) =>
for {
firstMatch <- this.matchParams(params)
ParamMatchOutput(q, remainingParams) = firstMatch
secondMatch <- that.matchParams(remainingParams)
ParamMatchOutput(r, finalRemainingParams) = secondMatch
} yield ParamMatchOutput(ev(q, r), finalRemainingParams),
(out: ev.Out) => {
val (q, r) = ev.unapply(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[A1 >: A](predicate: Q => Boolean, error: Map[String, Param] => A1): QueryParameters[Q, A1] = 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)
)
/**
* Associates this [[QueryParameters]] with the given [[Fragment]] in order to match raw urls satisfying both
* conditions, and returning the outputs from both.
*
* The path part of the url will be *ignored* (and will return Unit).
*/
final def withFragment[FragmentType, FragmentError](
fragment: Fragment[FragmentType, FragmentError]
): PathQueryFragmentRepr[Unit, Nothing, Q, A, FragmentType, FragmentError] =
new PathQueryFragmentRepr(PathSegment.root, this, fragment)
}
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: QueryParameters[Unit, Nothing] = factory[Unit, Nothing](
(params: Map[String, Param]) => Right(ParamMatchOutput((), params)),
_ => Map()
)
/** Alias for empty which seems to better reflect the semantic. */
final def ignore: QueryParameters[Unit, Nothing] = empty
final def simpleQueryParam[Q, A](
paramName: String,
matching: Param => Either[A, Q],
creating: Q => Param
)(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(Left(paramMatchingError.missingParameterError(paramName))),
creating.andThen(paramName -> _).andThen(Map(_))
)
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 => 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(List[Q]())
case Param(head :: tail) =>
tail
.map(fromString.apply)
.foldLeft(fromString(head).map(List(_))) { (acc, next) =>
for {
firstResults <- acc
nextResult <- next
} yield nextResult +: firstResults
}
.map(_.reverse)
},
q => Param(q.map(printer.apply))
)
final lazy val dummyErrorImpl = QueryParametersImpl[DummyError]
final lazy val simpleParamErrorImpl = QueryParametersImpl[SimpleParamMatchingError]
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy