1+ public import Algorithms
2+ import Foundation
13import Logging
24
35/// Generic ``TrieRouter`` built using the "trie" tree algorithm.
@@ -37,29 +39,22 @@ public final class TrieRouter<Output: Sendable>: Router, Sendable, CustomStringC
3739 /// - path: Raw path segments.
3840 /// - parameters: Will collect dynamic parameter values.
3941 /// - Returns: Output of matching route, if found.
40- @inlinable public func route ( path : [ String ] , parameters : inout Parameters ) -> Output ? {
41- // always start at the root node
42+ @inlinable
43+ public func route ( path : [ String ] , parameters : inout Parameters ) -> Output ? {
4244 var currentNode = self . root
43-
4445 let isCaseInsensitive = self . options. contains ( . caseInsensitive)
45-
4646 var currentCatchall : ( Node , [ String ] ) ?
4747
48- // traverse the string path supplied
49- search: for (index, slice) in path. enumerated ( ) {
50- // store catchall in case search hits dead end
48+ search: for (index, slice) in path. indexed ( ) {
5149 if let catchall = currentNode. catchall {
5250 currentCatchall = ( catchall, [ String] ( path. dropFirst ( index) ) )
5351 }
5452
55- // check the constants first
5653 if let constant = currentNode. constants [ isCaseInsensitive ? slice. lowercased ( ) : slice] {
5754 currentNode = constant
5855 continue search
5956 }
6057
61- // no constants matched, check for dynamic members
62- // including parameters or anythings
6358 if let wildcard = currentNode. wildcard {
6459 if let name = wildcard. parameter {
6560 parameters. set ( name, to: slice)
@@ -69,9 +64,19 @@ public final class TrieRouter<Output: Sendable>: Router, Sendable, CustomStringC
6964 continue search
7065 }
7166
72- // no matches, stop searching
67+ if let partials = currentNode. partials, !partials. isEmpty {
68+ for partial in partials {
69+ if let captures = isMatchForPartial ( partial: partial, path: slice, parameters: parameters) {
70+ for (name, value) in captures {
71+ parameters. set ( String ( name) , to: String ( value) )
72+ }
73+ currentNode = partial. node
74+ continue search
75+ }
76+ }
77+ }
78+
7379 if let ( catchall, subpaths) = currentCatchall {
74- // fallback to catchall output if we have one
7580 parameters. setCatchall ( matched: subpaths)
7681 return catchall. output
7782 } else {
@@ -80,14 +85,11 @@ public final class TrieRouter<Output: Sendable>: Router, Sendable, CustomStringC
8085 }
8186
8287 if let output = currentNode. output {
83- // return the currently resolved responder if there hasn't been an early exit.
8488 return output
8589 } else if let ( catchall, subpaths) = currentCatchall {
86- // fallback to catchall output if we have one
8790 parameters. setCatchall ( matched: subpaths)
8891 return catchall. output
8992 } else {
90- // current node has no output and there was not catchall
9193 return nil
9294 }
9395 }
@@ -96,4 +98,46 @@ public final class TrieRouter<Output: Sendable>: Router, Sendable, CustomStringC
9698 public var description : String {
9799 self . root. description
98100 }
101+
102+ @usableFromInline
103+ func isMatchForPartial( partial: Node . PartialMatch , path: String , parameters: Parameters ) -> [ Substring : Substring ] ? {
104+ var result : [ Substring : Substring ] = [ : ]
105+ var index = path. startIndex
106+
107+ var componentIndex = partial. components. startIndex
108+ let lastComponentIndex = partial. components. index ( before: partial. components. endIndex)
109+
110+ while componentIndex <= lastComponentIndex {
111+ if index >= path. endIndex {
112+ // If we're at the end but there are more components, fail
113+ if componentIndex < lastComponentIndex { return nil }
114+ break
115+ }
116+
117+ let element = partial. components [ componentIndex]
118+
119+ if element. isEmpty {
120+ let endIndex : String . Index
121+ if componentIndex < lastComponentIndex {
122+ let nextElement = partial. components [ partial. components. index ( after: componentIndex) ]
123+ // greedy matching
124+ guard let range = path. range ( of: nextElement, options: . backwards, range: index..< path. endIndex) else { return nil }
125+ endIndex = range. lowerBound
126+ } else {
127+ endIndex = path. endIndex
128+ }
129+ result [ partial. parameters [ result. count] ] = path [ index..< endIndex]
130+ index = endIndex
131+ } else {
132+ // Verify the literal matches at current position
133+ let substring = path [ index... ] . prefix ( element. count)
134+ guard substring == element else { return nil }
135+ index = substring. endIndex
136+ }
137+
138+ partial. components. formIndex ( after: & componentIndex)
139+ }
140+
141+ return result
142+ }
99143}
0 commit comments