@@ -5,43 +5,123 @@ import com.google.api.HttpRule
5
5
import org .http4s .{Method , Request , Uri }
6
6
import org .ivovk .connect_rpc_scala
7
7
import org .ivovk .connect_rpc_scala .grpc .MethodRegistry
8
+ import org .ivovk .connect_rpc_scala .http .json .JsonProcessing .*
8
9
import org .json4s .JsonAST .{JField , JObject }
9
10
import org .json4s .{JString , JValue }
10
11
11
- import scala .util .boundary
12
- import scala .util .boundary .break
12
+ import scala .jdk .CollectionConverters .*
13
13
14
- case class MatchedRequest (method : MethodRegistry .Entry , json : JValue )
14
+ case class MatchedRequest (
15
+ method : MethodRegistry .Entry ,
16
+ pathJson : JValue ,
17
+ queryJson : JValue ,
18
+ )
15
19
16
20
object TranscodingUrlMatcher {
17
21
case class Entry (
18
22
method : MethodRegistry .Entry ,
19
- httpMethodMatcher : Method => Boolean ,
23
+ httpMethod : Option [ Method ] ,
20
24
pattern : Uri .Path ,
21
25
)
22
26
27
+ sealed trait RouteTree
28
+
29
+ case class RootNode (
30
+ children : Vector [RouteTree ],
31
+ ) extends RouteTree
32
+
33
+ case class Node (
34
+ isVariable : Boolean ,
35
+ segment : String ,
36
+ children : Vector [RouteTree ],
37
+ ) extends RouteTree
38
+
39
+ case class Leaf (
40
+ httpMethod : Option [Method ],
41
+ method : MethodRegistry .Entry ,
42
+ ) extends RouteTree
43
+
44
+ private def mkTree (entries : Seq [Entry ]): Vector [RouteTree ] = {
45
+ entries.groupByOrd(_.pattern.segments.headOption)
46
+ .flatMap { (maybeSegment, entries) =>
47
+ maybeSegment match {
48
+ case None =>
49
+ entries.map { entry =>
50
+ Leaf (entry.httpMethod, entry.method)
51
+ }
52
+ case Some (head) =>
53
+ val variableDef = this .isVariable(head)
54
+ val segment =
55
+ if variableDef then
56
+ head.encoded.substring(1 , head.encoded.length - 1 )
57
+ else head.encoded
58
+
59
+ List (
60
+ Node (
61
+ variableDef,
62
+ segment,
63
+ mkTree(entries.map(e => e.copy(pattern = e.pattern.splitAt(1 )._2)).toVector),
64
+ )
65
+ )
66
+ }
67
+ }
68
+ .toVector
69
+ }
70
+
71
+ extension [A ](it : Iterable [A ]) {
72
+ // Preserves ordering of elements
73
+ def groupByOrd [B ](f : A => B ): Map [B , Vector [A ]] = {
74
+ val result = collection.mutable.LinkedHashMap .empty[B , Vector [A ]]
75
+
76
+ it.foreach { elem =>
77
+ val key = f(elem)
78
+ val vec = result.getOrElse(key, Vector .empty)
79
+ result.update(key, vec :+ elem)
80
+ }
81
+
82
+ result.toMap
83
+ }
84
+
85
+ // Returns the first element that is Some
86
+ def colFirst [B ](f : A => Option [B ]): Option [B ] = {
87
+ val iter = it.iterator
88
+ while (iter.hasNext) {
89
+ val x = f(iter.next())
90
+ if x.isDefined then return x
91
+ }
92
+ None
93
+ }
94
+ }
95
+
96
+ private def isVariable (segment : Uri .Path .Segment ): Boolean = {
97
+ val enc = segment.encoded
98
+ val length = enc.length
99
+
100
+ length > 2 && enc(0 ) == '{' && enc(length - 1 ) == '}'
101
+ }
102
+
23
103
def create [F [_]](
24
104
methods : Seq [MethodRegistry .Entry ],
25
105
pathPrefix : Uri .Path ,
26
106
): TranscodingUrlMatcher [F ] = {
27
107
val entries = methods.flatMap { method =>
28
- method.httpRule match {
29
- case Some (httpRule) =>
30
- val (httpMethod, pattern) = extractMethodAndPattern(httpRule)
108
+ method.httpRule.fold(List .empty[Entry ]) { httpRule =>
109
+ val additionalBindings = httpRule.getAdditionalBindingsList.asScala.toList
31
110
32
- val httpMethodMatcher : Method => Boolean = m => httpMethod.forall(_ == m)
111
+ (httpRule :: additionalBindings).map { rule =>
112
+ val (httpMethod, pattern) = extractMethodAndPattern(rule)
33
113
34
114
Entry (
35
115
method,
36
- httpMethodMatcher ,
37
- pathPrefix.dropEndsWithSlash. concat(pattern.toRelative)
38
- ).some
39
- case None => none
116
+ httpMethod ,
117
+ pathPrefix.concat(pattern),
118
+ )
119
+ }
40
120
}
41
121
}
42
122
43
123
new TranscodingUrlMatcher (
44
- entries,
124
+ RootNode (mkTree( entries)) ,
45
125
)
46
126
}
47
127
@@ -62,56 +142,40 @@ object TranscodingUrlMatcher {
62
142
}
63
143
64
144
class TranscodingUrlMatcher [F [_]](
65
- entries : Seq [ TranscodingUrlMatcher .Entry ] ,
145
+ tree : TranscodingUrlMatcher .RootNode ,
66
146
) {
67
147
68
- import org . ivovk . connect_rpc_scala . http . json . JsonProcessing .*
148
+ import TranscodingUrlMatcher .*
69
149
70
- def matchesRequest (req : Request [F ]): Option [MatchedRequest ] = boundary {
71
- entries.foreach { entry =>
72
- if (entry.httpMethodMatcher(req.method)) {
73
- matchExtract(entry.pattern, req.uri.path) match {
74
- case Some (pathParams) =>
75
- val queryParams = req.uri.query.toList.map((k, v) => k -> JString (v.getOrElse( " " )))
150
+ def matchesRequest (req : Request [F ]): Option [MatchedRequest ] = {
151
+ def doMatch ( node : RouteTree , path : List [ Uri . Path . Segment ], pathVars : List [ JField ]) : Option [ MatchedRequest ] = {
152
+ node match {
153
+ case Node (isVariable, patternSegment, children) if path.nonEmpty =>
154
+ val pathSegment = path.head
155
+ val pathTail = path.tail
76
156
77
- val merged = mergeFields(groupFields(pathParams), groupFields(queryParams))
157
+ if isVariable then
158
+ val newPatchVars = (patternSegment -> JString (pathSegment.encoded)) :: pathVars
78
159
79
- break(Some (MatchedRequest (entry.method, JObject (merged))))
80
- case None => // continue
81
- }
160
+ children.colFirst(doMatch(_, pathTail, newPatchVars))
161
+ else if pathSegment.encoded == patternSegment then
162
+ children.colFirst(doMatch(_, pathTail, pathVars))
163
+ else none
164
+ case Leaf (httpMethod, method) if path.isEmpty && httpMethod.forall(_ == req.method) =>
165
+ val queryParams = req.uri.query.toList.map((k, v) => k -> JString (v.getOrElse(" " )))
166
+
167
+ MatchedRequest (
168
+ method,
169
+ JObject (groupFields(pathVars)),
170
+ JObject (groupFields(queryParams))
171
+ ).some
172
+ case RootNode (children) =>
173
+ children.colFirst(doMatch(_, path, pathVars))
174
+ case _ => none
82
175
}
83
176
}
84
177
85
- none
178
+ doMatch(tree, req.uri.path.segments.toList, List .empty)
86
179
}
87
180
88
- /**
89
- * Matches path segments with pattern segments and extracts variables from the path.
90
- * Returns None if the path does not match the pattern.
91
- */
92
- private def matchExtract (pattern : Uri .Path , path : Uri .Path ): Option [List [JField ]] = boundary {
93
- if path.segments.length != pattern.segments.length then boundary.break(none)
94
-
95
- path.segments.indices
96
- .foldLeft(List .empty[JField ]) { (state, idx) =>
97
- val pathSegment = path.segments(idx)
98
- val patternSegment = pattern.segments(idx)
99
-
100
- if isVariable(patternSegment) then
101
- val varName = patternSegment.encoded.substring(1 , patternSegment.encoded.length - 1 )
102
-
103
- (varName -> JString (pathSegment.encoded)) :: state
104
- else if pathSegment != patternSegment then
105
- boundary.break(none)
106
- else state
107
- }
108
- .some
109
- }
110
-
111
- private def isVariable (segment : Uri .Path .Segment ): Boolean = {
112
- val enc = segment.encoded
113
- val length = enc.length
114
-
115
- length > 2 && enc(0 ) == '{' && enc(length - 1 ) == '}'
116
- }
117
181
}
0 commit comments