Skip to content

Commit a6f6c67

Browse files
authored
Merge pull request #41 from frzi/fix/issue-40
Fix/issue 40
2 parents b7600df + fb251b5 commit a6f6c67

File tree

2 files changed

+33
-23
lines changed

2 files changed

+33
-23
lines changed

Sources/Route.swift

+25-17
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ import SwiftUI
6363
///
6464
/// - Note: A `Route`'s default path is `*`, meaning it will always match.
6565
public struct Route<ValidatedData, Content: View>: View {
66-
66+
6767
public typealias Validator = (RouteInformation) -> ValidatedData?
6868

6969
@Environment(\.relativePath) private var relativePath
7070
@EnvironmentObject private var navigator: Navigator
7171
@EnvironmentObject private var switchEnvironment: SwitchRoutesEnvironment
7272
@StateObject private var pathMatcher = PathMatcher()
73-
73+
7474
private let content: (ValidatedData) -> Content
7575
private let path: String
7676
private let validator: Validator
@@ -111,7 +111,7 @@ public struct Route<ValidatedData, Content: View>: View {
111111
{
112112
validatedData = validated
113113
routeInformation = matchInformation
114-
114+
115115
if switchEnvironment.isActive {
116116
switchEnvironment.isResolved = true
117117
}
@@ -143,15 +143,15 @@ public extension Route where ValidatedData == RouteInformation {
143143
self.validator = { $0 }
144144
self.content = content
145145
}
146-
146+
147147
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
148148
/// - Parameter content: Views to render.
149149
init(_ path: String = "*", @ViewBuilder content: @escaping () -> Content) {
150150
self.path = path
151151
self.validator = { $0 }
152152
self.content = { _ in content() }
153153
}
154-
154+
155155
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
156156
/// - Parameter content: View to render (autoclosure).
157157
init(_ path: String = "*", content: @autoclosure @escaping () -> Content) {
@@ -190,13 +190,13 @@ public extension Route where ValidatedData == RouteInformation {
190190
public final class RouteInformation: ObservableObject {
191191
/// The resolved path component of the parent `Route`. For internal use only, at the moment.
192192
let matchedPath: String
193-
193+
194194
/// The current relative path.
195195
public let path: String
196196

197197
/// Resolved parameters of the parent `Route`s path.
198198
public let parameters: [String : String]
199-
199+
200200
init(path: String, matchedPath: String, parameters: [String : String] = [:]) {
201201
self.matchedPath = matchedPath
202202
self.parameters = parameters
@@ -215,11 +215,11 @@ final class PathMatcher: ObservableObject {
215215
let matchRegex: NSRegularExpression
216216
let parameters: Set<String>
217217
}
218-
218+
219219
private enum CompileError: Error {
220220
case badParameter(String, culprit: String)
221221
}
222-
222+
223223
private static let variablesRegex = try! NSRegularExpression(pattern: #":([^\/\?]+)"#, options: [])
224224

225225
//
@@ -261,19 +261,21 @@ final class PathMatcher: ObservableObject {
261261
var pattern = glob
262262
.replacingOccurrences(of: "^[^/]/$", with: "", options: .regularExpression) // Trailing slash.
263263
.replacingOccurrences(of: #"\/?\*"#, with: "", options: .regularExpression) // Trailing asterisk.
264-
265-
for variable in variables {
264+
265+
for (index, variable) in variables.enumerated() {
266+
let isAtRoot = index == 0 && glob.starts(with: "/:" + variable)
266267
pattern = pattern.replacingOccurrences(
267268
of: "/:" + variable,
268-
with: "/(?<" + variable + ">[^/?]+)", // Named capture group.
269+
with: (isAtRoot ? "/" : "") + "(?<\(variable)>" + (isAtRoot ? "" : "/?") + "[^/?]+)",
269270
options: .regularExpression)
270271
}
272+
271273
pattern = "^" +
272274
(pattern.isEmpty ? "" : "(\(pattern))") +
273275
(endsWithAsterisk ? "(/.*)?$" : "$")
274276

275277
let regex = try NSRegularExpression(pattern: pattern, options: [])
276-
278+
277279
cached = CompiledRegex(path: glob, matchRegex: regex, parameters: variables)
278280

279281
return cached!
@@ -288,7 +290,7 @@ final class PathMatcher: ObservableObject {
288290
if matches.isEmpty {
289291
return nil
290292
}
291-
293+
292294
var parameterValues: [String : String] = [:]
293295

294296
if !compiled.parameters.isEmpty {
@@ -297,11 +299,17 @@ final class PathMatcher: ObservableObject {
297299
if nsrange.location != NSNotFound,
298300
let range = Range(nsrange, in: path)
299301
{
300-
parameterValues[variable] = String(path[range])
302+
var value = String(path[range])
303+
304+
if value.starts(with: "/") {
305+
value = String(value.dropFirst())
306+
}
307+
308+
parameterValues[variable] = value
301309
}
302310
}
303311
}
304-
312+
305313
// Resolve the glob to get a new relative path.
306314
// We only want the part the glob is directly referencing.
307315
// I.e., if the glob is `/news/article/*` and the navigation path is `/news/article/1/details`,
@@ -312,7 +320,7 @@ final class PathMatcher: ObservableObject {
312320
else {
313321
return nil
314322
}
315-
323+
316324
let resolvedGlob = String(path[range])
317325
let matchedPath = String(path[relative.endIndex...])
318326

Tests/SwiftUIRouterTests/SwiftUIRouterTests.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ final class SwiftUIRouterTests: XCTestCase {
5757

5858
/// Test if the globs and paths match.
5959
func testCorrectMatches() {
60-
let pathMatcher = PathMatcher()
61-
6260
let notNil: [(String, String)] = [
6361
("/", "/"),
6462
("/*", "/"),
@@ -73,13 +71,17 @@ final class SwiftUIRouterTests: XCTestCase {
7371
("/news/latest", "/news/latest"),
7472
("/user/:id/*", "/user/1"),
7573
("/user/:id/*", "/user/1/settings"),
74+
("/user/:id?", "/user"),
75+
("/user/:id?", "/user/mark"),
76+
("/user/:id/:group?", "/user/mark"),
77+
("/user/:id/:group?", "/user/mark/admin"),
7678
]
7779

7880
for (glob, path) in notNil {
79-
let resolvedGlob = resolvePaths("/", glob)
81+
let pathMatcher = PathMatcher()
8082

8183
XCTAssertNotNil(
82-
try? pathMatcher.match(glob: resolvedGlob, with: path),
84+
try? pathMatcher.match(glob: glob, with: path),
8385
"Glob \(glob) does not match \(path)."
8486
)
8587
}
@@ -93,7 +95,7 @@ final class SwiftUIRouterTests: XCTestCase {
9395
let isNil: [(String, String)] = [
9496
("/", "/hello"),
9597
("/hello", "/world"),
96-
("/foo/:bar?/hello", "/foo/hello"),
98+
("/foo/:bar/hello", "/foo/hello"),
9799
("/movie", "/movies"),
98100
("/movie/*", "/movies"),
99101
("/movie/*", "/movies/actor"),
@@ -116,7 +118,7 @@ final class SwiftUIRouterTests: XCTestCase {
116118
("/:id?", "/hello", ["id": "hello"]),
117119
("/:id", "/hello", ["id": "hello"]),
118120
("/:foo/:bar", "/hello/world", ["foo": "hello", "bar": "world"]),
119-
("/:foo/:bar?", "/hello/", ["foo": "hello"]),
121+
("/:foo/:bar?", "/hello", ["foo": "hello"]),
120122
("/user/:id/*", "/user/5", ["id": "5"]),
121123
]
122124

0 commit comments

Comments
 (0)