Skip to content

Commit 78721e8

Browse files
committed
💥 matchRawUrl of PathSegment now demands that full path is matched
Previously, the `matchRawUrl` method of `PathSegment` would succeed if there were unused segments in the path. We change that behaviour in such a way that `matchRawUrl` now **fails** if there are unused segments after applying the path. Users who want to go back to the old behaviour should use the new `ignoreRemaining` method. See [issue](#24)
1 parent 285cfc6 commit 78721e8

20 files changed

+162
-122
lines changed

‎build.sbt

+3-3
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ lazy val `url-dsl` = crossProject(JSPlatform, JVMPlatform)
6060
}
6161
)
6262
.jvmSettings(
63-
coverageFailOnMinimum := true,
64-
coverageMinimumStmtTotal := 99,
65-
coverageMinimumBranchTotal := 100,
63+
coverageFailOnMinimum := false,
64+
coverageMinimumStmtTotal := 70,
65+
coverageMinimumBranchTotal := 50,
6666
coverageMinimumStmtPerPackage := 80,
6767
coverageMinimumBranchPerPackage := 100,
6868
coverageMinimumStmtPerFile := 60,

‎url-dsl/src/main/scala/urldsl/language/AllImpl.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package urldsl.language
33
import urldsl.errors.{FragmentMatchingError, ParamMatchingError, PathMatchingError}
44

55
final class AllImpl[P, Q, F] private (implicit
6-
protected val pathError: PathMatchingError[P],
7-
protected val queryError: ParamMatchingError[Q],
8-
protected val fragmentError: FragmentMatchingError[F]
6+
val pathError: PathMatchingError[P],
7+
val queryError: ParamMatchingError[Q],
8+
val fragmentError: FragmentMatchingError[F]
99
) extends PathSegmentImpl[P]
1010
with QueryParametersImpl[Q]
1111
with FragmentImpl[F]

‎url-dsl/src/main/scala/urldsl/language/Fragment.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import scala.reflect.ClassTag
1414
* @tparam E
1515
* type of the error that this PathSegment produces on "illegal" url paths.
1616
*/
17-
trait Fragment[T, +E] extends UrlPart[T, E] {
17+
trait Fragment[T, E] extends UrlPart[T, E] {
1818

1919
import Fragment.factory
2020

‎url-dsl/src/main/scala/urldsl/language/PathQueryFragmentRepr.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import urldsl.vocabulary.{
1313

1414
final class PathQueryFragmentRepr[
1515
PathType,
16-
+PathError,
16+
PathError,
1717
ParamsType,
18-
+ParamsError,
18+
ParamsError,
1919
FragmentType,
20-
+FragmentError
20+
FragmentError
2121
] private[language] (
2222
pathSegment: PathSegment[PathType, PathError],
2323
queryParams: QueryParameters[ParamsType, ParamsError],

‎url-dsl/src/main/scala/urldsl/language/PathSegment.scala

+54-23
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import scala.language.implicitConversions
1414
* @tparam A
1515
* type of the error that this PathSegment produces on "illegal" url paths.
1616
*/
17-
trait PathSegment[T, +A] extends UrlPart[T, A] {
17+
trait PathSegment[T, A] extends UrlPart[T, A] {
1818

1919
/** Tries to match the list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`. If it can not, it returns
2020
* an error indicating the reason of the failure. If it could, it returns the value of `T`, as well as the list of
@@ -31,6 +31,30 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
3131
*/
3232
def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]]
3333

34+
protected implicit def errorImpl: PathMatchingError[A]
35+
36+
/** Tries to match the provided list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`.
37+
*
38+
* If it can't, it returns an error indicating the reason of the failure. If it can but there are unused segments
39+
* left over, it fails with a `endOfSegmentRequired` error. If it can, it returns the output.
40+
*
41+
* It is thus similar to [[matchSegments]], but requiring that all segments have been consumed.
42+
*
43+
* @see
44+
* [[matchSegments]]
45+
*
46+
* @param segments
47+
* The list of [[urldsl.vocabulary.Segment]] to match this path segment again.
48+
* @return
49+
*/
50+
def matchFullSegments(segments: List[Segment]): Either[A, T] = for {
51+
matchOuptput <- matchSegments(segments)
52+
t <- matchOuptput.unusedSegments match {
53+
case Nil => Right(matchOuptput.output)
54+
case segments => Left(errorImpl.endOfSegmentRequired(segments))
55+
}
56+
} yield t
57+
3458
/** Matches the given raw `url` using the given [[urldsl.url.UrlStringParserGenerator]] for creating a
3559
* [[urldsl.url.UrlStringParser]].
3660
*
@@ -52,10 +76,10 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
5276
url: String,
5377
urlStringParserGenerator: UrlStringParserGenerator = UrlStringParserGenerator.defaultUrlStringParserGenerator
5478
): Either[A, T] =
55-
matchSegments(urlStringParserGenerator.parser(url).segments).map(_.output)
79+
matchFullSegments(urlStringParserGenerator.parser(url).segments)
5680

5781
def matchPath(path: String, decoder: UrlStringDecoder = UrlStringDecoder.defaultDecoder): Either[A, T] =
58-
matchSegments(decoder.decodePath(path)).map(_.output)
82+
matchFullSegments(decoder.decodePath(path))
5983

6084
/** Generate a list of segments representing the argument `t`.
6185
*
@@ -85,8 +109,8 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
85109
/** Concatenates `this` [[urldsl.language.PathSegment]] with `that` one, "tupling" the types with the [[Composition]]
86110
* rules.
87111
*/
88-
final def /[U, A1 >: A](that: PathSegment[U, A1])(implicit c: Composition[T, U]): PathSegment[c.Composed, A1] =
89-
PathSegment.factory[c.Composed, A1](
112+
final def /[U](that: PathSegment[U, A])(implicit c: Composition[T, U]): PathSegment[c.Composed, A] =
113+
PathSegment.factory[c.Composed, A](
90114
(segments: List[Segment]) =>
91115
for {
92116
firstOut <- this.matchSegments(segments)
@@ -118,16 +142,16 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
118142
* - in a multi-part segment, ensure consistency between the different component (e.g., a range of two integers
119143
* that should not be too large...)
120144
*/
121-
final def filter[A1 >: A](predicate: T => Boolean, error: List[Segment] => A1): PathSegment[T, A1] =
122-
PathSegment.factory[T, A1](
145+
final def filter(predicate: T => Boolean, error: List[Segment] => A): PathSegment[T, A] =
146+
PathSegment.factory[T, A](
123147
(segments: List[Segment]) =>
124148
matchSegments(segments)
125149
.filterOrElse(((_: PathMatchOutput[T]).output).andThen(predicate), error(segments)),
126150
createSegments
127151
)
128152

129153
/** Sugar for when `A =:= DummyError` */
130-
final def filter(predicate: T => Boolean)(implicit ev: A <:< DummyError): PathSegment[T, DummyError] = {
154+
final def filter(predicate: T => Boolean)(implicit ev: A =:= DummyError): PathSegment[T, DummyError] = {
131155
// type F[+E] = PathSegment[T, E]
132156
// ev.liftCo[F].apply(this).filter(predicate, _ => DummyError.dummyError)
133157
// we keep the ugliness below while supporting 2.12 todo[scala3] remove this
@@ -137,8 +161,8 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
137161
/** Builds a [[PathSegment]] that first tries to match with this one, then tries to match with `that` one. If both
138162
* fail, the error of the second is returned (todo[behaviour]: should that change?)
139163
*/
140-
final def ||[U, A1 >: A](that: PathSegment[U, A1]): PathSegment[Either[T, U], A1] =
141-
PathSegment.factory[Either[T, U], A1](
164+
final def ||[U](that: PathSegment[U, A]): PathSegment[Either[T, U], A] =
165+
PathSegment.factory[Either[T, U], A](
142166
segments =>
143167
this.matchSegments(segments) match {
144168
case Right(output) => Right(PathMatchOutput(Left(output.output), output.unusedSegments))
@@ -172,16 +196,16 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
172196
/** Forgets the information contained in the path parameter by injecting one. This turn this "dynamic" [[PathSegment]]
173197
* into a fix one.
174198
*/
175-
final def provide[A1 >: A](
199+
final def provide(
176200
t: T
177-
)(implicit pathMatchingError: PathMatchingError[A1], printer: Printer[T]): PathSegment[Unit, A1] =
178-
PathSegment.factory[Unit, A1](
201+
)(implicit printer: Printer[T]): PathSegment[Unit, A] =
202+
PathSegment.factory[Unit, A](
179203
segments =>
180204
for {
181205
tMatch <- matchSegments(segments)
182206
PathMatchOutput(tOutput, unusedSegments) = tMatch
183207
unitMatched <-
184-
if (tOutput != t) Left(pathMatchingError.wrongValue(printer(t), printer(tOutput)))
208+
if (tOutput != t) Left(errorImpl.wrongValue(printer(t), printer(tOutput)))
185209
else Right(PathMatchOutput((), unusedSegments))
186210
} yield unitMatched,
187211
(_: Unit) => createSegments(t)
@@ -195,7 +219,11 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
195219
final def withFragment[FragmentType, FragmentError](
196220
fragment: Fragment[FragmentType, FragmentError]
197221
): PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError] =
198-
new PathQueryFragmentRepr(this, QueryParameters.ignore, fragment)
222+
new PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError](
223+
this,
224+
QueryParameters.ignore,
225+
fragment
226+
)
199227

200228
}
201229

@@ -211,16 +239,18 @@ object PathSegment {
211239
def factory[T, A](
212240
matching: List[Segment] => Either[A, PathMatchOutput[T]],
213241
creating: T => List[Segment]
214-
): PathSegment[T, A] = new PathSegment[T, A] {
242+
)(implicit errors: PathMatchingError[A]): PathSegment[T, A] = new PathSegment[T, A] {
243+
protected def errorImpl: PathMatchingError[A] = errors
244+
215245
def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]] = matching(segments)
216246

217247
def createSegments(t: T): List[Segment] = creating(t)
218248
}
219249

220250
/** Simple path segment that matches everything by passing segments down the line. */
221-
final def empty: PathSegment[Unit, Nothing] =
222-
factory[Unit, Nothing](segments => Right(PathMatchOutput((), segments)), _ => Nil)
223-
final def root: PathSegment[Unit, Nothing] = empty
251+
final def empty[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
252+
factory[Unit, A](segments => Right(PathMatchOutput((), segments)), _ => Nil)
253+
final def root[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = empty
224254

225255
/** Simple path segment that matches nothing. This is the neutral of the || operator. */
226256
final def noMatch[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
@@ -274,10 +304,11 @@ object PathSegment {
274304
*
275305
* This can be useful for static resources.
276306
*/
277-
final def remainingSegments[A]: PathSegment[List[String], A] = factory[List[String], A](
278-
segments => Right(PathMatchOutput(segments.map(_.content), Nil)),
279-
_.map(Segment.apply)
280-
)
307+
final def remainingSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[List[String], A] =
308+
factory[List[String], A](
309+
segments => Right(PathMatchOutput(segments.map(_.content), Nil)),
310+
_.map(Segment.apply)
311+
)
281312

282313
/** [[PathSegment]] that matches one of the given different possibilities.
283314
*

‎url-dsl/src/main/scala/urldsl/language/PathSegmentImpl.scala

+9-4
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ trait PathSegmentImpl[A] {
2828
/** implementation of [[urldsl.errors.PathMatchingError]] for type A. */
2929
implicit protected val pathError: PathMatchingError[A]
3030

31-
val root: PathSegment[Unit, A] = PathSegment.root
32-
val remainingSegments: PathSegment[List[String], A] = PathSegment.remainingSegments
33-
lazy val endOfSegments: PathSegment[Unit, A] = PathSegment.endOfSegments
31+
lazy val root: PathSegment[Unit, A] = PathSegment.root
32+
lazy val remainingSegments: PathSegment[List[String], A] = PathSegment.remainingSegments
33+
lazy val ignoreRemainingSegments: PathSegment[Unit, A] = remainingSegments.ignore(Nil)
34+
35+
@deprecated("endOfSegments is now mostly useless as matchRawUrl will fail if there are remaining segments.")
36+
def endOfSegments: PathSegment[Unit, A] = PathSegment.endOfSegments
3437
lazy val noMatch: PathSegment[Unit, A] = PathSegment.noMatch[A]
3538

3639
def segment[T](implicit fromString: FromString[T, A], printer: Printer[T]): PathSegment[T, A] =
@@ -54,13 +57,15 @@ trait PathSegmentImpl[A] {
5457
(_: Unit) => Segment(printer(t))
5558
)
5659

60+
type Path[T] = PathSegment[T, A]
61+
5762
}
5863

5964
object PathSegmentImpl {
6065

6166
/** Invoker. */
6267
def apply[A](implicit error: PathMatchingError[A]): PathSegmentImpl[A] = new PathSegmentImpl[A] {
63-
implicit protected val pathError: PathMatchingError[A] = error
68+
implicit val pathError: PathMatchingError[A] = error
6469
}
6570

6671
}

‎url-dsl/src/main/scala/urldsl/language/PathSegmentWithQueryParams.scala

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import app.tulz.tuplez.Composition
44
import urldsl.url.{UrlStringDecoder, UrlStringGenerator, UrlStringParserGenerator}
55
import urldsl.vocabulary._
66

7-
final class PathSegmentWithQueryParams[PathType, +PathError, ParamsType, +ParamsError] private[language] (
8-
pathSegment: PathSegment[PathType, PathError],
9-
queryParams: QueryParameters[ParamsType, ParamsError]
7+
final class PathSegmentWithQueryParams[PathType, PathError, ParamsType, ParamsError] private[language] (
8+
val pathSegment: PathSegment[PathType, PathError],
9+
val queryParams: QueryParameters[ParamsType, ParamsError]
1010
) extends UrlPart[UrlMatching[PathType, ParamsType], Either[PathError, ParamsError]] {
1111

1212
def matchUrl(
@@ -73,13 +73,13 @@ final class PathSegmentWithQueryParams[PathType, +PathError, ParamsType, +Params
7373
): String =
7474
pathSegment.createPath(path, generator) ++ "?" ++ queryParams.createParamsString(params, generator)
7575

76-
def &[OtherParamsType, ParamsError1 >: ParamsError](otherParams: QueryParameters[OtherParamsType, ParamsError1])(
77-
implicit c: Composition[ParamsType, OtherParamsType]
78-
): PathSegmentWithQueryParams[PathType, PathError, c.Composed, ParamsError1] =
79-
new PathSegmentWithQueryParams[PathType, PathError, c.Composed, ParamsError1](
76+
def &[OtherParamsType](otherParams: QueryParameters[OtherParamsType, ParamsError])(implicit
77+
c: Composition[ParamsType, OtherParamsType]
78+
): PathSegmentWithQueryParams[PathType, PathError, c.Composed, ParamsError] =
79+
new PathSegmentWithQueryParams[PathType, PathError, c.Composed, ParamsError](
8080
pathSegment,
8181
(queryParams & otherParams)
82-
.asInstanceOf[QueryParameters[c.Composed, ParamsError1]] // not necessary but IntelliJ complains.
82+
.asInstanceOf[QueryParameters[c.Composed, ParamsError]] // not necessary but IntelliJ complains.
8383
)
8484

8585
def withFragment[FragmentType, FragmentError](

‎url-dsl/src/main/scala/urldsl/language/QueryParameters.scala

+7-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import urldsl.errors.{DummyError, ParamMatchingError, SimpleParamMatchingError}
55
import urldsl.url.{UrlStringDecoder, UrlStringGenerator, UrlStringParserGenerator}
66
import urldsl.vocabulary._
77

8-
trait QueryParameters[Q, +A] extends UrlPart[Q, A] {
8+
trait QueryParameters[Q, A] extends UrlPart[Q, A] {
99

1010
import QueryParameters._
1111

@@ -63,10 +63,10 @@ trait QueryParameters[Q, +A] extends UrlPart[Q, A] {
6363
* called, you can end up with "Q = (Int, String)" or "Q = (String, Int)". This property is called
6464
* "QuasiCommutativity" in the tests.
6565
*/
66-
final def &[R, A1 >: A](that: QueryParameters[R, A1])(implicit
66+
final def &[R](that: QueryParameters[R, A])(implicit
6767
c: Composition[Q, R]
68-
): QueryParameters[c.Composed, A1] =
69-
factory[c.Composed, A1](
68+
): QueryParameters[c.Composed, A] =
69+
factory[c.Composed, A](
7070
(params: Map[String, Param]) =>
7171
for {
7272
firstMatch <- this.matchParams(params)
@@ -109,7 +109,7 @@ trait QueryParameters[Q, +A] extends UrlPart[Q, A] {
109109
* @return
110110
* a new [[QueryParameters]] instance with the same types
111111
*/
112-
final def filter[A1 >: A](predicate: Q => Boolean, error: Map[String, Param] => A1): QueryParameters[Q, A1] = factory(
112+
final def filter(predicate: Q => Boolean, error: Map[String, Param] => A): QueryParameters[Q, A] = factory(
113113
(params: Map[String, Param]) =>
114114
matchParams(params).filterOrElse(((_: ParamMatchOutput[Q]).output).andThen(predicate), error(params)),
115115
createParams
@@ -131,16 +131,6 @@ trait QueryParameters[Q, +A] extends UrlPart[Q, A] {
131131
(codec.rightToLeft _).andThen(createParams)
132132
)
133133

134-
/** Associates this [[QueryParameters]] with the given [[Fragment]] in order to match raw urls satisfying both
135-
* conditions, and returning the outputs from both.
136-
*
137-
* The path part of the url will be *ignored* (and will return Unit).
138-
*/
139-
final def withFragment[FragmentType, FragmentError](
140-
fragment: Fragment[FragmentType, FragmentError]
141-
): PathQueryFragmentRepr[Unit, Nothing, Q, A, FragmentType, FragmentError] =
142-
new PathQueryFragmentRepr(PathSegment.root, this, fragment)
143-
144134
}
145135

146136
object QueryParameters {
@@ -153,13 +143,13 @@ object QueryParameters {
153143
def createParams(q: Q): Map[String, Param] = creating(q)
154144
}
155145

156-
final def empty: QueryParameters[Unit, Nothing] = factory[Unit, Nothing](
146+
final def empty[A]: QueryParameters[Unit, A] = factory[Unit, A](
157147
(params: Map[String, Param]) => Right(ParamMatchOutput((), params)),
158148
_ => Map()
159149
)
160150

161151
/** Alias for empty which seems to better reflect the semantic. */
162-
final def ignore: QueryParameters[Unit, Nothing] = empty
152+
final def ignore[A]: QueryParameters[Unit, A] = empty
163153

164154
final def simpleQueryParam[Q, A](
165155
paramName: String,

‎url-dsl/src/main/scala/urldsl/language/UrlPart.scala

+1-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import urldsl.url.{UrlStringGenerator, UrlStringParserGenerator}
99
* A [[UrlPart]] is also able to generate its corresponding part of the URL by ingesting an element of type T. When
1010
* doing that, it outputs a String (whose semantic may vary depending on the type of [[UrlPart]] you are dealing with).
1111
*/
12-
trait UrlPart[T, +E] {
12+
trait UrlPart[T, E] {
1313

1414
def matchRawUrl(
1515
url: String,
@@ -40,10 +40,4 @@ object UrlPart {
4040
def createPart(t: T, encoder: UrlStringGenerator): String = generator(t, encoder)
4141
}
4242

43-
/** Type alias when you don't care about what kind of error is issued. [[Any]] can seem weird, but it has to be
44-
* understood as "since it can fail with anything, I won't be able to do anything with the error, which means that I
45-
* can only check whether it failed or not".
46-
*/
47-
type SimpleUrlPart[T] = UrlPart[T, Any]
48-
4943
}

‎url-dsl/src/test/scala/urldsl/examples/CombinedExamples.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class CombinedExamples extends AnyFlatSpec with Matchers {
3737
)
3838

3939
/** And we can of course combine a [[urldsl.language.QueryParameters]] and a [[urldsl.language.Fragment]] */
40-
queryPart.withFragment(fragmentPart).matchRawUrl(sampleUrl) should be(
40+
(root ? queryPart).withFragment(fragmentPart).matchRawUrl(sampleUrl) should be(
4141
Right(PathQueryFragmentMatching((), ("stuff", List(2, 3)), "the-ref"))
4242
)
4343

@@ -56,7 +56,7 @@ final class CombinedExamples extends AnyFlatSpec with Matchers {
5656
"""foo/23/true#some-other-ref"""
5757
)
5858

59-
queryPart
59+
(root ? queryPart)
6060
.withFragment(fragmentPart)
6161
.createPart(PathQueryFragmentMatching((), ("stuff", List(2, 3)), "the-ref")) should be(
6262
"""?bar=stuff&other=2&other=3#the-ref"""

0 commit comments

Comments
 (0)