/
PathMatcher.scala
553 lines (493 loc) · 20 KB
/
PathMatcher.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
/*
* Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.http.scaladsl.server
import java.util.UUID
import scala.util.matching.Regex
import scala.annotation.tailrec
import akka.http.scaladsl.server.util.Tuple
import akka.http.scaladsl.server.util.TupleOps._
import akka.http.scaladsl.common.NameOptionReceptacle
import akka.http.scaladsl.model.Uri.Path
import akka.http.impl.util._
/**
* A PathMatcher tries to match a prefix of a given string and returns either a PathMatcher.Matched instance
* if matched, otherwise PathMatcher.Unmatched.
*/
abstract class PathMatcher[L](implicit val ev: Tuple[L]) extends (Path => PathMatcher.Matching[L]) { self =>
import PathMatcher._
def / : PathMatcher[L] = this ~ PathMatchers.Slash
def /[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] =
this ~ PathMatchers.Slash ~ other
def |[R >: L: Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] =
new PathMatcher[R] {
def apply(path: Path) = self(path) orElse other(path)
}
def ~[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = {
implicit val joinProducesTuple = Tuple.yes[join.Out]
transform(_.andThen((restL, valuesL) => other(restL).map(join(valuesL, _))))
}
def unary_!(): PathMatcher0 =
new PathMatcher[Unit] {
def apply(path: Path) = if (self(path) eq Unmatched) Matched(path, ()) else Unmatched
}
def transform[R: Tuple](f: Matching[L] => Matching[R]): PathMatcher[R] =
new PathMatcher[R] { def apply(path: Path) = f(self(path)) }
def tmap[R: Tuple](f: L => R): PathMatcher[R] = transform(_.map(f))
def tflatMap[R: Tuple](f: L => Option[R]): PathMatcher[R] = transform(_.flatMap(f))
/**
* Same as `repeat(min = count, max = count)`.
*/
def repeat(count: Int)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count)
/**
* Same as `repeat(min = count, max = count, separator = separator)`.
*/
def repeat(count: Int, separator: PathMatcher0)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count, separator = separator)
/**
* Turns this `PathMatcher` into one that matches a number of times (with the given separator)
* and potentially extracts a `List` of the underlying matcher's extractions.
* If less than `min` applications of the underlying matcher have succeeded the produced matcher fails,
* otherwise it matches up to the given `max` number of applications.
* Note that it won't fail even if more than `max` applications could succeed!
* The "surplus" path elements will simply be left unmatched.
*
* The result type depends on the type of the underlying matcher:
*
* <table>
* <th><td>If a `matcher` is of type</td><td>then `matcher.repeat(...)` is of type</td></th>
* <tr><td>`PathMatcher0`</td><td>`PathMatcher0`</td></tr>
* <tr><td>`PathMatcher1[T]`</td><td>`PathMatcher1[List[T]`</td></tr>
* <tr><td>`PathMatcher[L :Tuple]`</td><td>`PathMatcher[List[L]]`</td></tr>
* </table>
*/
def repeat(min: Int, max: Int, separator: PathMatcher0 = PathMatchers.Neutral)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
require(min >= 0, "`min` must be >= 0")
require(max >= min, "`max` must be >= `min`")
def apply(path: Path) = matchNext(path, 0)
def matchNext(path: Path, alreadyFound: Int): Matching[lift.Out] = {
def done = if (alreadyFound >= min) Matched(path, lift()) else Unmatched
def matchSeparatorIfNeeded(path: Path): Matching[Unit] =
if (alreadyFound == 0) Matched(path, ()) else separator(path)
def matchElement(start: Path): Matching[lift.Out] =
self(start)
.andThen { (remaining, extractions) =>
matchNext(remaining, alreadyFound + 1)
.map(result => lift(extractions, result))
}
.orElse(done)
if (alreadyFound < max)
matchSeparatorIfNeeded(path)
.andThen { (remaining, _) => matchElement(remaining) }
.orElse(done)
else done
}
}
}
object PathMatcher extends ImplicitPathMatcherConstruction {
sealed abstract class Matching[+L: Tuple] {
def map[R: Tuple](f: L => R): Matching[R]
def flatMap[R: Tuple](f: L => Option[R]): Matching[R]
def andThen[R: Tuple](f: (Path, L) => Matching[R]): Matching[R]
def orElse[R >: L](other: => Matching[R]): Matching[R]
}
case class Matched[L: Tuple](pathRest: Path, extractions: L) extends Matching[L] {
def map[R: Tuple](f: L => R) = Matched(pathRest, f(extractions))
def flatMap[R: Tuple](f: L => Option[R]) = f(extractions) match {
case Some(valuesR) => Matched(pathRest, valuesR)
case None => Unmatched
}
def andThen[R: Tuple](f: (Path, L) => Matching[R]) = f(pathRest, extractions)
def orElse[R >: L](other: => Matching[R]) = this
}
object Matched { val Empty = Matched(Path.Empty, ()) }
case object Unmatched extends Matching[Nothing] {
def map[R: Tuple](f: Nothing => R) = this
def flatMap[R: Tuple](f: Nothing => Option[R]) = this
def andThen[R: Tuple](f: (Path, Nothing) => Matching[R]) = this
def orElse[R](other: => Matching[R]) = other
}
/**
* Creates a PathMatcher that always matches, consumes nothing and extracts the given Tuple of values.
*/
def provide[L: Tuple](extractions: L): PathMatcher[L] =
new PathMatcher[L] {
def apply(path: Path) = Matched(path, extractions)(ev)
}
/**
* Creates a PathMatcher that matches and consumes the given path prefix and extracts the given list of extractions.
* If the given prefix is empty the returned PathMatcher matches always and consumes nothing.
*/
def apply[L: Tuple](prefix: Path, extractions: L): PathMatcher[L] =
if (prefix.isEmpty) provide(extractions)
else new PathMatcher[L] {
def apply(path: Path) =
if (path startsWith prefix) Matched(path dropChars prefix.charCount, extractions)(ev)
else Unmatched
}
/** Provoke implicit conversions to PathMatcher to be applied */
def apply[L](magnet: PathMatcher[L]): PathMatcher[L] = magnet
implicit class PathMatcher1Ops[T](matcher: PathMatcher1[T]) {
def map[R](f: T => R): PathMatcher1[R] = matcher.tmap { case Tuple1(e) => Tuple1(f(e)) }
def flatMap[R](f: T => Option[R]): PathMatcher1[R] =
matcher.tflatMap { case Tuple1(e) => f(e).map(x => Tuple1(x)) }
}
implicit class EnhancedPathMatcher[L](underlying: PathMatcher[L]) {
def ?(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
def apply(path: Path) = underlying(path) match {
case Matched(rest, extractions) => Matched(rest, lift(extractions))
case Unmatched => Matched(path, lift())
}
}
}
sealed trait Lift[L, M[+_]] {
type Out
def OutIsTuple: Tuple[Out]
def apply(): Out
def apply(value: L): Out
def apply(value: L, more: Out): Out
}
object Lift extends LowLevelLiftImplicits {
trait MOps[M[+_]] {
def apply(): M[Nothing]
def apply[T](value: T): M[T]
def apply[T](value: T, more: M[T]): M[T]
}
object MOps {
implicit val OptionMOps: MOps[Option] =
new MOps[Option] {
def apply(): Option[Nothing] = None
def apply[T](value: T): Option[T] = Some(value)
def apply[T](value: T, more: Option[T]): Option[T] = Some(value)
}
implicit val ListMOps: MOps[List] =
new MOps[List] {
def apply(): List[Nothing] = Nil
def apply[T](value: T): List[T] = value :: Nil
def apply[T](value: T, more: List[T]): List[T] = value :: more
}
}
implicit def liftUnit[M[+_]]: Lift[Unit, M] { type Out = Unit } =
new Lift[Unit, M] {
type Out = Unit
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = ()
def apply(value: Unit) = value
def apply(value: Unit, more: Out) = value
}
implicit def liftSingleElement[A, M[+_]](implicit mops: MOps[M]): Lift[Tuple1[A], M] { type Out = Tuple1[M[A]] } =
new Lift[Tuple1[A], M] {
type Out = Tuple1[M[A]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: Tuple1[A]) = Tuple1(mops(value._1))
def apply(value: Tuple1[A], more: Out) = Tuple1(mops(value._1, more._1))
}
}
trait LowLevelLiftImplicits {
import Lift._
implicit def default[T, M[+_]](implicit mops: MOps[M]): Lift[T, M] { type Out = Tuple1[M[T]] } =
new Lift[T, M] {
type Out = Tuple1[M[T]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: T) = Tuple1(mops(value))
def apply(value: T, more: Out) = Tuple1(mops(value, more._1))
}
}
/** The empty match returned when a Regex matcher matches the empty path */
private[http] val EmptyMatch = Matched(Path.Empty, Tuple1(""))
}
/**
* @groupname pathmatcherimpl Path matcher implicits
* @groupprio pathmatcherimpl 172
*/
trait ImplicitPathMatcherConstruction {
import PathMatcher._
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment) and extracts a given value.
*
* @group pathmatcherimpl
*/
implicit def _stringExtractionPair2PathMatcher[T](tuple: (String, T)): PathMatcher1[T] =
PathMatcher(tuple._1 :: Path.Empty, Tuple1(tuple._2))
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment).
*
* @group pathmatcherimpl
*/
implicit def _segmentStringToPathMatcher(segment: String): PathMatcher0 =
PathMatcher(segment :: Path.Empty, ())
/**
* @group pathmatcherimpl
*/
implicit def _stringNameOptionReceptacle2PathMatcher(nr: NameOptionReceptacle[String]): PathMatcher0 =
PathMatcher(nr.name).?
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* if the path begins with a segment (a prefix of) which matches the given regex.
* Extracts either the complete match (if the regex doesn't contain a capture group) or
* the capture group (if the regex contains exactly one).
* If the regex contains more than one capture group the method throws an IllegalArgumentException.
*
* @group pathmatcherimpl
*/
implicit def _regex2PathMatcher(regex: Regex): PathMatcher1[String] = {
lazy val matchesEmptyPath = "" match {
case `regex`(_*) => true
case _ => false
}
regex.groupCount match {
case 0 => new PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) => regex findPrefixOf segment match {
case Some(m) => Matched(segment.substring(m.length) :: tail, Tuple1(m))
case None => Unmatched
}
case Path.Empty if matchesEmptyPath => PathMatcher.EmptyMatch
case _ => Unmatched
}
}
case 1 => new PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) => regex findPrefixMatchOf segment match {
case Some(m) => Matched(segment.substring(m.end) :: tail, Tuple1(m.group(1)))
case None => Unmatched
}
case Path.Empty if matchesEmptyPath => PathMatcher.EmptyMatch
case _ => Unmatched
}
}
case _ => throw new IllegalArgumentException("Path regex '" + regex.pattern.pattern +
"' must not contain more than one capturing group")
}
}
/**
* Creates a PathMatcher from the given Map of path segments (prefixes) to extracted values.
* If the unmatched path starts with a segment having one of the maps keys as a prefix
* the matcher consumes this path segment (prefix) and extracts the corresponding map value.
* For keys sharing a common prefix the longest matching prefix is selected.
*
* @group pathmatcherimpl
*/
implicit def _valueMap2PathMatcher[T](valueMap: Map[String, T]): PathMatcher1[T] =
if (valueMap.isEmpty) PathMatchers.nothingMatcher
else valueMap.toSeq.sortWith(_._1 > _._1).map(_stringExtractionPair2PathMatcher).reduceLeft(_ | _)
}
/**
* @groupname pathmatcher Path matchers
* @groupprio pathmatcher 171
*/
trait PathMatchers {
import PathMatcher._
/**
* Converts a path string containing slashes into a PathMatcher that interprets slashes as
* path segment separators.
*
* @group pathmatcher
*/
def separateOnSlashes(string: String): PathMatcher0 = {
@tailrec def split(ix: Int = 0, matcher: PathMatcher0 = null): PathMatcher0 = {
val nextIx = string.indexOf('/', ix)
def append(m: PathMatcher0) = if (matcher eq null) m else matcher / m
if (nextIx < 0) append(string.substring(ix))
else split(nextIx + 1, append(string.substring(ix, nextIx)))
}
split()
}
/**
* A PathMatcher that matches a single slash character ('/').
*
* @group pathmatcher
*/
object Slash extends PathMatcher0 {
def apply(path: Path) = path match {
case Path.Slash(tail) => Matched(tail, ())
case _ => Unmatched
}
}
/**
* A PathMatcher that matches the very end of the requests URI path.
*
* @group pathmatcher
*/
object PathEnd extends PathMatcher0 {
def apply(path: Path) = path match {
case Path.Empty => Matched.Empty
case _ => Unmatched
}
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path as an (encoded!) String.
* If you need access to the remaining unencoded elements of the path
* use the `RemainingPath` matcher!
*
* @group pathmatcher
*/
object Remaining extends PathMatcher1[String] {
def apply(path: Path) = Matched(Path.Empty, Tuple1(path.toString))
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path.
*
* @group pathmatcher
*/
object RemainingPath extends PathMatcher1[Path] {
def apply(path: Path) = Matched(Path.Empty, Tuple1(path))
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*
* @group pathmatcher
*/
object IntNumber extends NumberMatcher[Int](Int.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*
* @group pathmatcher
*/
object LongNumber extends NumberMatcher[Long](Long.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*
* @group pathmatcher
*/
object HexIntNumber extends NumberMatcher[Int](Int.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*
* @group pathmatcher
*/
object HexLongNumber extends NumberMatcher[Long](Long.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
// common implementation of Number matchers
/**
* @group pathmatcher
*/
abstract class NumberMatcher[@specialized(Int, Long) T](max: T, base: T)(implicit x: Integral[T])
extends PathMatcher1[T] {
import x._ // import implicit conversions for numeric operators
val minusOne = x.zero - x.one
val maxDivBase = max / base
def apply(path: Path) = path match {
case Path.Segment(segment, tail) =>
@tailrec def digits(ix: Int = 0, value: T = minusOne): Matching[Tuple1[T]] = {
val a = if (ix < segment.length) fromChar(segment charAt ix) else minusOne
if (a == minusOne) {
if (value == minusOne) Unmatched
else Matched(if (ix < segment.length) segment.substring(ix) :: tail else tail, Tuple1(value))
} else {
if (value == minusOne) digits(ix + 1, a)
else if (value <= maxDivBase && value * base <= max - a) // protect from overflow
digits(ix + 1, value * base + a)
else Unmatched
}
}
digits()
case _ => Unmatched
}
def fromChar(c: Char): T
def fromDecimalChar(c: Char): T = if ('0' <= c && c <= '9') x.fromInt(c - '0') else minusOne
def fromHexChar(c: Char): T =
if ('0' <= c && c <= '9') x.fromInt(c - '0') else {
val cn = c | 0x20 // normalize to lowercase
if ('a' <= cn && cn <= 'f') x.fromInt(cn - 'a' + 10) else minusOne
}
}
/**
* A PathMatcher that matches and extracts a Double value. The matched string representation is the pure decimal,
* optionally signed form of a double value, i.e. without exponent.
*
* @group pathmatcher
*/
val DoubleNumber: PathMatcher1[Double] =
PathMatcher("""[+-]?\d*\.?\d*""".r) flatMap { string =>
try Some(java.lang.Double.parseDouble(string))
catch { case _: NumberFormatException => None }
}
/**
* A PathMatcher that matches and extracts a java.util.UUID instance.
*
* @group pathmatcher
*/
val JavaUUID: PathMatcher1[UUID] =
PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string =>
try Some(UUID.fromString(string))
catch { case _: IllegalArgumentException => None }
}
/**
* A PathMatcher that always matches, doesn't consume anything and extracts nothing.
* Serves mainly as a neutral element in PathMatcher composition.
*
* @group pathmatcher
*/
val Neutral: PathMatcher0 = PathMatcher.provide(())
/**
* A PathMatcher that matches if the unmatched path starts with a path segment.
* If so the path segment is extracted as a String.
*
* @group pathmatcher
*/
object Segment extends PathMatcher1[String] {
def apply(path: Path) = path match {
case Path.Segment(segment, tail) => Matched(tail, Tuple1(segment))
case _ => Unmatched
}
}
/**
* A PathMatcher that matches up to 128 remaining segments as a List[String].
* This can also be no segments resulting in the empty list.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
val Segments: PathMatcher1[List[String]] = Segments(min = 0, max = 128)
/**
* A PathMatcher that matches the given number of path segments (separated by slashes) as a List[String].
* If there are more than `count` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
def Segments(count: Int): PathMatcher1[List[String]] = Segment.repeat(count, separator = Slash)
/**
* A PathMatcher that matches between `min` and `max` (both inclusively) path segments (separated by slashes)
* as a List[String]. If there are more than `count` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
def Segments(min: Int, max: Int): PathMatcher1[List[String]] = Segment.repeat(min, max, separator = Slash)
/**
* A PathMatcher that never matches anything.
*
* @group pathmatcher
*/
def nothingMatcher[L: Tuple]: PathMatcher[L] =
new PathMatcher[L] {
def apply(p: Path) = Unmatched
}
}
object PathMatchers extends PathMatchers