Skip to content

Commit 5847668

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 5847668

16 files changed

+145
-89
lines changed

‎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

+56-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))
@@ -169,19 +193,21 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
169193
(_: Unit) => createSegments(default)
170194
)
171195

196+
final def ignoreRemaining: PathSegment[T, A] = this / PathSegment.remainingSegments.ignore(Nil)
197+
172198
/** Forgets the information contained in the path parameter by injecting one. This turn this "dynamic" [[PathSegment]]
173199
* into a fix one.
174200
*/
175-
final def provide[A1 >: A](
201+
final def provide(
176202
t: T
177-
)(implicit pathMatchingError: PathMatchingError[A1], printer: Printer[T]): PathSegment[Unit, A1] =
178-
PathSegment.factory[Unit, A1](
203+
)(implicit printer: Printer[T]): PathSegment[Unit, A] =
204+
PathSegment.factory[Unit, A](
179205
segments =>
180206
for {
181207
tMatch <- matchSegments(segments)
182208
PathMatchOutput(tOutput, unusedSegments) = tMatch
183209
unitMatched <-
184-
if (tOutput != t) Left(pathMatchingError.wrongValue(printer(t), printer(tOutput)))
210+
if (tOutput != t) Left(errorImpl.wrongValue(printer(t), printer(tOutput)))
185211
else Right(PathMatchOutput((), unusedSegments))
186212
} yield unitMatched,
187213
(_: Unit) => createSegments(t)
@@ -195,7 +221,11 @@ trait PathSegment[T, +A] extends UrlPart[T, A] {
195221
final def withFragment[FragmentType, FragmentError](
196222
fragment: Fragment[FragmentType, FragmentError]
197223
): PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError] =
198-
new PathQueryFragmentRepr(this, QueryParameters.ignore, fragment)
224+
new PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError](
225+
this,
226+
QueryParameters.ignore,
227+
fragment
228+
)
199229

200230
}
201231

@@ -211,16 +241,18 @@ object PathSegment {
211241
def factory[T, A](
212242
matching: List[Segment] => Either[A, PathMatchOutput[T]],
213243
creating: T => List[Segment]
214-
): PathSegment[T, A] = new PathSegment[T, A] {
244+
)(implicit errors: PathMatchingError[A]): PathSegment[T, A] = new PathSegment[T, A] {
245+
protected def errorImpl: PathMatchingError[A] = errors
246+
215247
def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]] = matching(segments)
216248

217249
def createSegments(t: T): List[Segment] = creating(t)
218250
}
219251

220252
/** 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
253+
final def empty[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
254+
factory[Unit, A](segments => Right(PathMatchOutput((), segments)), _ => Nil)
255+
final def root[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = empty
224256

225257
/** Simple path segment that matches nothing. This is the neutral of the || operator. */
226258
final def noMatch[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
@@ -274,10 +306,11 @@ object PathSegment {
274306
*
275307
* This can be useful for static resources.
276308
*/
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-
)
309+
final def remainingSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[List[String], A] =
310+
factory[List[String], A](
311+
segments => Right(PathMatchOutput(segments.map(_.content), Nil)),
312+
_.map(Segment.apply)
313+
)
281314

282315
/** [[PathSegment]] that matches one of the given different possibilities.
283316
*

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ 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
31+
lazy val root: PathSegment[Unit, A] = PathSegment.root
32+
lazy val remainingSegments: PathSegment[List[String], A] = PathSegment.remainingSegments
33+
lazy val ignoreRemaining: PathSegment[Unit, A] = remainingSegments.ignore(Nil)
3334
lazy val endOfSegments: PathSegment[Unit, A] = PathSegment.endOfSegments
3435
lazy val noMatch: PathSegment[Unit, A] = PathSegment.noMatch[A]
3536

@@ -54,13 +55,15 @@ trait PathSegmentImpl[A] {
5455
(_: Unit) => Segment(printer(t))
5556
)
5657

58+
type Path[T] = PathSegment[T, A]
59+
5760
}
5861

5962
object PathSegmentImpl {
6063

6164
/** Invoker. */
6265
def apply[A](implicit error: PathMatchingError[A]): PathSegmentImpl[A] = new PathSegmentImpl[A] {
63-
implicit protected val pathError: PathMatchingError[A] = error
66+
implicit val pathError: PathMatchingError[A] = error
6467
}
6568

6669
}

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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] (
7+
final class PathSegmentWithQueryParams[PathType, PathError, ParamsType, ParamsError] private[language] (
88
pathSegment: PathSegment[PathType, PathError],
99
queryParams: QueryParameters[ParamsType, ParamsError]
1010
) extends UrlPart[UrlMatching[PathType, ParamsType], Either[PathError, ParamsError]] {
@@ -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)