
urldsl.language.PathSegment.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 Show documentation
Show all versions of url-dsl_3 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, ErrorFromThrowable, PathMatchingError, SimplePathMatchingError}
import urldsl.url.{UrlStringDecoder, UrlStringGenerator, UrlStringParserGenerator}
import urldsl.vocabulary._
import app.tulz.tuplez.Composition
import scala.language.implicitConversions
/** Represents a part of the path string of an URL, containing an information of type T, or an error of type A.
* @tparam T
* type represented by this PathSegment
* @tparam A
* type of the error that this PathSegment produces on "illegal" url paths.
*/
trait PathSegment[T, A] extends UrlPart[T, A] {
/** Tries to match the list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`. If it can not, it returns
* an error indicating the reason of the failure. If it could, it returns the value of `T`, as well as the list of
* unused segments.
*
* @example
* For example, a segment that matches simply a String in the first segment, when giving segments like
* List(Segment("hello"), Segment("3")) will return Right(PathMatchOutput("hello", List(Segment("3")))
*
* @param segments
* The list of [[urldsl.vocabulary.Segment]] to match this path segment again.
* @return
* The "de-serialized" element with unused segment, if successful.
*/
def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]]
protected implicit def errorImpl: PathMatchingError[A]
/** Tries to match the provided list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`.
*
* If it can't, it returns an error indicating the reason of the failure. If it can but there are unused segments
* left over, it fails with a `endOfSegmentRequired` error. If it can, it returns the output.
*
* It is thus similar to [[matchSegments]], but requiring that all segments have been consumed.
*
* @see
* [[matchSegments]]
*
* @param segments
* The list of [[urldsl.vocabulary.Segment]] to match this path segment again.
* @return
*/
def matchFullSegments(segments: List[Segment]): Either[A, T] = for {
matchOuptput <- matchSegments(segments)
t <- matchOuptput.unusedSegments match {
case Nil => Right(matchOuptput.output)
case segments => Left(errorImpl.endOfSegmentRequired(segments))
}
} yield t
/** Matches the given raw `url` using the given [[urldsl.url.UrlStringParserGenerator]] for creating a
* [[urldsl.url.UrlStringParser]].
*
* This method doesn't return the information about the remaining unused segments. The thought leading to this is
* that [[urldsl.vocabulary.PathMatchOutput]] are supposed to be internal mechanics, while this method is supposed to
* be the exposed interface of this [[urldsl.language.PathSegment]].
*
* @param url
* the url to parse. It has to be a well formed URL, otherwise this could raise an exception, depending on the
* provided [[urldsl.url.UrlStringParserGenerator]].
* @param urlStringParserGenerator
* the [[urldsl.url.UrlStringParserGenerator]] used to create the [[urldsl.url.UrlStringParser]] that will actually
* parse the url to create the segments. The default one is usually a good choice. It has different implementations
* in JVM and JS, but they *should* behave the same way.
* @return
* the output contained in the url, or the error if something fails.
*/
def matchRawUrl(
url: String,
urlStringParserGenerator: UrlStringParserGenerator = UrlStringParserGenerator.defaultUrlStringParserGenerator
): Either[A, T] =
matchFullSegments(urlStringParserGenerator.parser(url).segments)
def matchPath(path: String, decoder: UrlStringDecoder = UrlStringDecoder.defaultDecoder): Either[A, T] =
matchFullSegments(decoder.decodePath(path))
/** Generate a list of segments representing the argument `t`.
*
* `matchSegments` and `createSegments` should be (functional) inverse of each other. That is,
* `this.matchSegments(this.createSegments(t)) == Right(PathMathOutput(t, Nil))`
*/
def createSegments(t: T): List[Segment]
/** Sugar when `T =:= Unit`
*/
final def createSegments()(implicit ev: Unit =:= T): List[Segment] = createSegments(ev(()))
/** Concatenates the segments generated by `createSegments`
*/
def createPath(t: T, encoder: UrlStringGenerator = UrlStringGenerator.default): String =
encoder.makePath(createSegments(t))
/** Sugar when `T =:= Unit`
*/
final def createPath()(implicit ev: Unit =:= T): String =
createPath(())
final def createPath(encoder: UrlStringGenerator)(implicit ev: Unit =:= T): String =
createPath((), encoder)
final def createPart(t: T, encoder: UrlStringGenerator): String = createPath(t, encoder)
/** Concatenates `this` [[urldsl.language.PathSegment]] with `that` one, "tupling" the types with the [[Composition]]
* rules.
*/
final def /[U](that: PathSegment[U, A])(implicit c: Composition[T, U]): PathSegment[c.Composed, A] =
PathSegment.factory[c.Composed, A](
(segments: List[Segment]) =>
for {
firstOut <- this.matchSegments(segments)
PathMatchOutput(t, remaining) = firstOut
secondOut <- that.matchSegments(remaining)
PathMatchOutput(u, lastRemaining) = secondOut
} yield PathMatchOutput(c.compose(t, u), lastRemaining),
(out: c.Composed) => {
val (t, u) = c.decompose(out)
this.createSegments(t) ++ that.createSegments(u)
}
)
final def ?[ParamsType, QPError](
params: QueryParameters[ParamsType, QPError]
): PathSegmentWithQueryParams[T, A, ParamsType, QPError] =
new PathSegmentWithQueryParams(this, params)
/** Adds an extra satisfying criteria to the de-serialized output of this [[urldsl.language.PathSegment]].
*
* The new de-serialization works as follows:
* - if the initial de-serialization fails, then it returns the generated error
* - otherwise, if the de-serialized element satisfies the predicate, then it returns the element
* - if the predicate is false, generates the given `error` by feeding it the segments that it tried to match.
*
* This can be useful in, among others, two scenarios:
* - enforce bigger restriction on a segment (e.g., from integers to positive integer, regex match...)
* - in a multi-part segment, ensure consistency between the different component (e.g., a range of two integers
* that should not be too large...)
*/
final def filter(predicate: T => Boolean, error: List[Segment] => A): PathSegment[T, A] =
PathSegment.factory[T, A](
(segments: List[Segment]) =>
matchSegments(segments)
.filterOrElse(((_: PathMatchOutput[T]).output).andThen(predicate), error(segments)),
createSegments
)
/** Sugar for when `A =:= DummyError` */
final def filter(predicate: T => Boolean)(implicit ev: A =:= DummyError): PathSegment[T, DummyError] = {
// type F[+E] = PathSegment[T, E]
// ev.liftCo[F].apply(this).filter(predicate, _ => DummyError.dummyError)
// we keep the ugliness below while supporting 2.12 todo[scala3] remove this
this.asInstanceOf[PathSegment[T, DummyError]].filter(predicate, _ => DummyError.dummyError)
}
/** Builds a [[PathSegment]] that first tries to match with this one, then tries to match with `that` one. If both
* fail, the error of the second is returned (todo[behaviour]: should that change?)
*/
final def ||[U](that: PathSegment[U, A]): PathSegment[Either[T, U], A] =
PathSegment.factory[Either[T, U], A](
segments =>
this.matchSegments(segments) match {
case Right(output) => Right(PathMatchOutput(Left(output.output), output.unusedSegments))
case Left(_) =>
that.matchSegments(segments).map(output => PathMatchOutput(Right(output.output), output.unusedSegments))
},
_.fold(this.createSegments, that.createSegments)
)
/** Casts this [[PathSegment]] to the new type U. Note that the [[urldsl.vocabulary.Codec]] must be an exception-free
* bijection between T and U (or at least an embedding, if you know that you are doing).
*/
final def as[U](implicit codec: Codec[T, U]): PathSegment[U, A] = as[U](codec.leftToRight _, codec.rightToLeft _)
/** Casts this [[PathSegment]] to the new type U. The conversion functions should form an exception-free bijection
* between T and U (or at least an embedding, if you know that you are doing).
*/
final def as[U](fromTToU: T => U, fromUToT: U => T): PathSegment[U, A] = PathSegment.factory[U, A](
(matchSegments _).andThen(_.map(_.map(fromTToU))),
fromUToT.andThen(createSegments)
)
/** Matches using this [[PathSegment]], and then forgets its content. Uses the `default` value when creating the path
* to go back.
*/
final def ignore(default: => T): PathSegment[Unit, A] = PathSegment.factory[Unit, A](
matchSegments(_).map(_.map(_ => ())),
(_: Unit) => createSegments(default)
)
/** Forgets the information contained in the path parameter by injecting one. This turn this "dynamic" [[PathSegment]]
* into a fix one.
*/
final def provide(
t: T
)(implicit printer: Printer[T]): PathSegment[Unit, A] =
PathSegment.factory[Unit, A](
segments =>
for {
tMatch <- matchSegments(segments)
PathMatchOutput(tOutput, unusedSegments) = tMatch
unitMatched <-
if (tOutput != t) Left(errorImpl.wrongValue(printer(t), printer(tOutput)))
else Right(PathMatchOutput((), unusedSegments))
} yield unitMatched,
(_: Unit) => createSegments(t)
)
/** Associates this [[PathSegment]] with the given [[Fragment]] in order to match raw urls satisfying both conditions,
* and returning the outputs from both.
*
* The query part of the url will be *ignored* (and will return Unit).
*/
final def withFragment[FragmentType, FragmentError](
fragment: Fragment[FragmentType, FragmentError]
): PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError] =
new PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError](
this,
QueryParameters.ignore,
fragment
)
}
object PathSegment {
type PathSegmentSimpleError[T] = PathSegment[T, SimplePathMatchingError]
/** A Type of path segment where we don't care about the error.
*/
type PathSegmentNoError[T] = PathSegment[T, DummyError]
/** Trait factory */
def factory[T, A](
matching: List[Segment] => Either[A, PathMatchOutput[T]],
creating: T => List[Segment]
)(implicit errors: PathMatchingError[A]): PathSegment[T, A] = new PathSegment[T, A] {
protected def errorImpl: PathMatchingError[A] = errors
def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]] = matching(segments)
def createSegments(t: T): List[Segment] = creating(t)
}
/** Simple path segment that matches everything by passing segments down the line. */
final def empty[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
factory[Unit, A](segments => Right(PathMatchOutput((), segments)), _ => Nil)
final def root[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = empty
/** Simple path segment that matches nothing. This is the neutral of the || operator. */
final def noMatch[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
factory[Unit, A](_ => Left(pathMatchingError.unit), _ => Nil)
/** Simple trait factory for "single segment"-oriented path Segments.
*
* This can be used to match a simple String, or a simple Int, etc...
*/
final def simplePathSegment[T, A](matching: Segment => Either[A, T], creating: T => Segment)(implicit
pathMatchingError: PathMatchingError[A]
): PathSegment[T, A] =
factory(
(_: Seq[Segment]) match {
case Nil => Left(pathMatchingError.missingSegment)
case first :: rest => matching(first).map(PathMatchOutput(_, rest))
},
List(_).map(creating)
)
/** Matches a simple String and returning it. */
final def stringSegment[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[String, A] =
segment[String, A]
/** Matches a simple Int and tries to convert it to an Int. */
final def intSegment[A](implicit
pathMatchingError: PathMatchingError[A],
fromThrowable: ErrorFromThrowable[A]
): PathSegment[Int, A] = segment[Int, A]
/** Creates a segment matching any element of type `T`, as long as the [[urldsl.vocabulary.FromString]] can
* de-serialize it.
*/
final def segment[T, A](implicit
fromString: FromString[T, A],
printer: Printer[T],
error: PathMatchingError[A]
): PathSegment[T, A] = simplePathSegment[T, A]((fromString.apply _).compose(_.content), printer.print)
/** Check that the segments ends at this point.
*/
final def endOfSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = factory[Unit, A](
(_: List[Segment]) match {
case Nil => Right(PathMatchOutput((), Nil))
case ss => Left(pathMatchingError.endOfSegmentRequired(ss))
},
_ => Nil
)
/** Consumes all the remaining segments.
*
* This can be useful for static resources.
*/
final def remainingSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[List[String], A] =
factory[List[String], A](
segments => Right(PathMatchOutput(segments.map(_.content), Nil)),
_.map(Segment.apply)
)
/** [[PathSegment]] that matches one of the given different possibilities.
*
* This can be useful in a Router, when you want to delegate the final decision to an inner router. Since all
* possibilities are good, the creation of segment simply takes the first one.
*/
final def oneOf[T, A](t: T, ts: T*)(implicit
fromString: FromString[T, A],
printer: Printer[T],
pathMatchingError: PathMatchingError[A]
): PathSegment[Unit, A] = {
val allTs = t +: ts.toList
simplePathSegment(
s =>
fromString(s.content)
.filterOrElse(
allTs.contains,
pathMatchingError.wrongValue("One of: " + allTs.map(printer.apply).mkString(", "), s.content)
)
.map(_ => ()),
(_: Unit) => t
)
}
/** Returns a [[urldsl.language.PathSegment]] which matches exactly the argument `t`.
*
* This conversion is implicit if you can provide a [[urldsl.vocabulary.FromString]] and a
* [[urldsl.vocabulary.Printer]], so that it enables writing, e.g., `root / "hello" / true`
*/
implicit final def unaryPathSegment[T, A](
t: T
)(implicit
fromString: FromString[T, A],
printer: Printer[T],
pathMatchingError: PathMatchingError[A]
): PathSegment[Unit, A] =
simplePathSegment(
s =>
fromString(s.content)
.filterOrElse[A](_ == t, pathMatchingError.wrongValue(printer(t), s.content))
.map(_ => ()),
(_: Unit) => Segment(printer(t))
)
final lazy val dummyErrorImpl = PathSegmentImpl[DummyError]
final lazy val simplePathErrorImpl = PathSegmentImpl[SimplePathMatchingError]
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy