Skip to content

Commit

Permalink
Merge pull request #41 from frzi/fix/issue-40
Browse files Browse the repository at this point in the history
Fix/issue 40
  • Loading branch information
frzi committed Dec 26, 2021
2 parents b7600df + fb251b5 commit a6f6c67
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 23 deletions.
42 changes: 25 additions & 17 deletions Sources/Route.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ import SwiftUI
///
/// - Note: A `Route`'s default path is `*`, meaning it will always match.
public struct Route<ValidatedData, Content: View>: View {

public typealias Validator = (RouteInformation) -> ValidatedData?

@Environment(\.relativePath) private var relativePath
@EnvironmentObject private var navigator: Navigator
@EnvironmentObject private var switchEnvironment: SwitchRoutesEnvironment
@StateObject private var pathMatcher = PathMatcher()

private let content: (ValidatedData) -> Content
private let path: String
private let validator: Validator
Expand Down Expand Up @@ -111,7 +111,7 @@ public struct Route<ValidatedData, Content: View>: View {
{
validatedData = validated
routeInformation = matchInformation

if switchEnvironment.isActive {
switchEnvironment.isResolved = true
}
Expand Down Expand Up @@ -143,15 +143,15 @@ public extension Route where ValidatedData == RouteInformation {
self.validator = { $0 }
self.content = content
}

/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
/// - Parameter content: Views to render.
init(_ path: String = "*", @ViewBuilder content: @escaping () -> Content) {
self.path = path
self.validator = { $0 }
self.content = { _ in content() }
}

/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
/// - Parameter content: View to render (autoclosure).
init(_ path: String = "*", content: @autoclosure @escaping () -> Content) {
Expand Down Expand Up @@ -190,13 +190,13 @@ public extension Route where ValidatedData == RouteInformation {
public final class RouteInformation: ObservableObject {
/// The resolved path component of the parent `Route`. For internal use only, at the moment.
let matchedPath: String

/// The current relative path.
public let path: String

/// Resolved parameters of the parent `Route`s path.
public let parameters: [String : String]

init(path: String, matchedPath: String, parameters: [String : String] = [:]) {
self.matchedPath = matchedPath
self.parameters = parameters
Expand All @@ -215,11 +215,11 @@ final class PathMatcher: ObservableObject {
let matchRegex: NSRegularExpression
let parameters: Set<String>
}

private enum CompileError: Error {
case badParameter(String, culprit: String)
}

private static let variablesRegex = try! NSRegularExpression(pattern: #":([^\/\?]+)"#, options: [])

//
Expand Down Expand Up @@ -261,19 +261,21 @@ final class PathMatcher: ObservableObject {
var pattern = glob
.replacingOccurrences(of: "^[^/]/$", with: "", options: .regularExpression) // Trailing slash.
.replacingOccurrences(of: #"\/?\*"#, with: "", options: .regularExpression) // Trailing asterisk.

for variable in variables {

for (index, variable) in variables.enumerated() {
let isAtRoot = index == 0 && glob.starts(with: "/:" + variable)
pattern = pattern.replacingOccurrences(
of: "/:" + variable,
with: "/(?<" + variable + ">[^/?]+)", // Named capture group.
with: (isAtRoot ? "/" : "") + "(?<\(variable)>" + (isAtRoot ? "" : "/?") + "[^/?]+)",
options: .regularExpression)
}

pattern = "^" +
(pattern.isEmpty ? "" : "(\(pattern))") +
(endsWithAsterisk ? "(/.*)?$" : "$")

let regex = try NSRegularExpression(pattern: pattern, options: [])

cached = CompiledRegex(path: glob, matchRegex: regex, parameters: variables)

return cached!
Expand All @@ -288,7 +290,7 @@ final class PathMatcher: ObservableObject {
if matches.isEmpty {
return nil
}

var parameterValues: [String : String] = [:]

if !compiled.parameters.isEmpty {
Expand All @@ -297,11 +299,17 @@ final class PathMatcher: ObservableObject {
if nsrange.location != NSNotFound,
let range = Range(nsrange, in: path)
{
parameterValues[variable] = String(path[range])
var value = String(path[range])

if value.starts(with: "/") {
value = String(value.dropFirst())
}

parameterValues[variable] = value
}
}
}

// Resolve the glob to get a new relative path.
// We only want the part the glob is directly referencing.
// I.e., if the glob is `/news/article/*` and the navigation path is `/news/article/1/details`,
Expand All @@ -312,7 +320,7 @@ final class PathMatcher: ObservableObject {
else {
return nil
}

let resolvedGlob = String(path[range])
let matchedPath = String(path[relative.endIndex...])

Expand Down
14 changes: 8 additions & 6 deletions Tests/SwiftUIRouterTests/SwiftUIRouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ final class SwiftUIRouterTests: XCTestCase {

/// Test if the globs and paths match.
func testCorrectMatches() {
let pathMatcher = PathMatcher()

let notNil: [(String, String)] = [
("/", "/"),
("/*", "/"),
Expand All @@ -73,13 +71,17 @@ final class SwiftUIRouterTests: XCTestCase {
("/news/latest", "/news/latest"),
("/user/:id/*", "/user/1"),
("/user/:id/*", "/user/1/settings"),
("/user/:id?", "/user"),
("/user/:id?", "/user/mark"),
("/user/:id/:group?", "/user/mark"),
("/user/:id/:group?", "/user/mark/admin"),
]

for (glob, path) in notNil {
let resolvedGlob = resolvePaths("/", glob)
let pathMatcher = PathMatcher()

XCTAssertNotNil(
try? pathMatcher.match(glob: resolvedGlob, with: path),
try? pathMatcher.match(glob: glob, with: path),
"Glob \(glob) does not match \(path)."
)
}
Expand All @@ -93,7 +95,7 @@ final class SwiftUIRouterTests: XCTestCase {
let isNil: [(String, String)] = [
("/", "/hello"),
("/hello", "/world"),
("/foo/:bar?/hello", "/foo/hello"),
("/foo/:bar/hello", "/foo/hello"),
("/movie", "/movies"),
("/movie/*", "/movies"),
("/movie/*", "/movies/actor"),
Expand All @@ -116,7 +118,7 @@ final class SwiftUIRouterTests: XCTestCase {
("/:id?", "/hello", ["id": "hello"]),
("/:id", "/hello", ["id": "hello"]),
("/:foo/:bar", "/hello/world", ["foo": "hello", "bar": "world"]),
("/:foo/:bar?", "/hello/", ["foo": "hello"]),
("/:foo/:bar?", "/hello", ["foo": "hello"]),
("/user/:id/*", "/user/5", ["id": "5"]),
]

Expand Down

0 comments on commit a6f6c67

Please sign in to comment.