Skip to content

Commit 75b5270

Browse files
committed
Merge branch 'develop'
2 parents e36c64b + ec83738 commit 75b5270

File tree

5 files changed

+229
-24
lines changed

5 files changed

+229
-24
lines changed

Sources/express/Route.swift

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Noze.io / Macro
44
//
55
// Created by Helge Heß on 6/2/16.
6-
// Copyright © 2016-2025 ZeeZide GmbH. All rights reserved.
6+
// Copyright © 2016-2026 ZeeZide GmbH. All rights reserved.
77
//
88

99
import enum NIOHTTP1.HTTPMethod
@@ -36,13 +36,13 @@ private let debugWalker = process.getenvflag("macro.router.walker.debug")
3636
* ## Path Patterns
3737
*
3838
* The Route accepts a pattern for the path:
39-
* - the "*" string is considered a match-all.
40-
* - otherwise the string is split into path components (on '/')
41-
* - if it starts with a "/", the pattern will start with a Root symbol
42-
* - "*" (like in `/users/ * / view`) matches any component (spaces added)
39+
* - the `*` string is considered a match-all.
40+
* - otherwise the string is split into path components (on `/`)
41+
* - if it starts with a `/`, the pattern will start with a Root symbol
42+
* - `*` (like in `/users/ * / view`) matches any component (spaces added)
4343
* - if the component starts with `:`, it is considered a variable.
4444
* Example: `/users/:id/view`
45-
* - "text*", "*text*", "*text" creates hasPrefix/hasSuffix/contains patterns
45+
* - `text*`, `*text*`, `*text` creates hasPrefix/hasSuffix/contains patterns
4646
* - otherwise the text is matched AS IS
4747
*
4848
* Variables can be extracted using:
@@ -66,6 +66,7 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
6666

6767
var id : String?
6868
let methods : ContiguousArray<HTTPMethod>?
69+
let exact : Bool
6970

7071
@inlinable
7172
public var isEmpty : Bool {
@@ -81,14 +82,18 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
8182
public init(id : String? = nil,
8283
pattern : String? = nil,
8384
method : HTTPMethod? = nil,
85+
exact : Bool? = nil,
8486
middleware : [ Middleware ] = [],
8587
errorMiddleware : [ ErrorMiddleware ] = [])
8688
{
8789
self.id = id
8890

89-
if let m = method { self.methods = [ m ] }
90-
else { self.methods = nil }
91-
91+
if let m = method { self.methods = [ m ] } else { self.methods = nil }
92+
93+
// Unless the user explicitly set `exact`, we are exact if a method is
94+
// specified, otherwise not.
95+
self.exact = exact ?? (method != nil)
96+
9297
self.middleware = middleware
9398
self.errorMiddleware = errorMiddleware
9499

@@ -102,14 +107,15 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
102107
}
103108

104109
@inlinable
105-
public convenience init(id : String? = nil,
106-
pattern : String? = nil,
107-
method : HTTPMethod? = nil,
108-
middleware : [ MiddlewareObject ])
110+
public convenience init(id : String? = nil,
111+
pattern : String? = nil,
112+
method : HTTPMethod? = nil,
113+
exact : Bool? = nil,
114+
middleware : [ MiddlewareObject ])
109115
{
110116
// In ExExpress we use an enum to hold the different variants, which might
111117
// be a little more efficient
112-
self.init(id: id, pattern: pattern, method: method,
118+
self.init(id: id, pattern: pattern, method: method, exact: exact,
113119
middleware: middleware.map { $0.middleware })
114120
}
115121

@@ -185,8 +191,9 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
185191
let mountPath = String(reqPath[base.endIndex..<reqPath.endIndex])
186192
let comps = split(urlPath: mountPath)
187193

188-
let mountMatchPath = RoutePattern.match(pattern : pattern,
189-
against : comps,
194+
let mountMatchPath = RoutePattern.match(pattern : pattern,
195+
against : comps,
196+
exact : exact,
190197
variables : &newParams)
191198
guard let match = mountMatchPath else {
192199
if debug {
@@ -203,11 +210,11 @@ open class Route: MiddlewareObject, ErrorMiddlewareObject, RouteKeeper,
203210

204211
guard let mp = RoutePattern.match(pattern : pattern,
205212
against : comps,
213+
exact : exact,
206214
variables : &newParams)
207215
else {
208216
if debug {
209-
console.log("\(ids) route path does not match, next:",
210-
self)
217+
console.log("\(ids) route path does not match, next:", self)
211218
}
212219
if let error = error { throw error }
213220
return upperNext()

Sources/express/RouteFactories.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public extension RouteKeeper {
4444
_ middleware: Middleware...) -> Self
4545
{
4646
add(route: Route(id: id, pattern: pathPattern, method: nil,
47-
middleware: middleware))
47+
exact: true, middleware: middleware))
4848
return self
4949
}
5050

@@ -187,10 +187,11 @@ public extension RouteKeeper {
187187
@discardableResult
188188
@inlinable
189189
func all(id: String? = nil, _ pathPattern: String,
190-
_ middleware: Middleware..., final: @escaping FinalMiddleware)
190+
_ middleware: Middleware..., final: @escaping FinalMiddleware)
191191
-> Self
192192
{
193193
add(route: Route(id: id, pattern: pathPattern, method: nil,
194+
exact: true,
194195
middleware: middleware + [ final2middleware(final) ]))
195196
return self
196197
}
@@ -352,7 +353,7 @@ public extension RouteKeeper {
352353
_ errorMiddleware: ErrorMiddleware...) -> Self
353354
{
354355
add(route: Route(id: id, pattern: pathPattern, method: nil,
355-
errorMiddleware: errorMiddleware))
356+
exact: true, errorMiddleware: errorMiddleware))
356357
return self
357358
}
358359

@@ -499,6 +500,7 @@ public extension RouteKeeper {
499500
async lastMiddleware: @escaping AsyncMiddleware) -> Self
500501
{
501502
add(route: Route(id: id, pattern: pathPattern, method: nil,
503+
exact: true,
502504
middleware: middleware
503505
+ asyncMiddleware.map(async)
504506
+ [ async(lastMiddleware) ]))
@@ -702,6 +704,7 @@ public extension RouteKeeper {
702704
asyncFinal: @escaping AsyncFinalMiddleware) -> Self
703705
{
704706
add(route: Route(id: id, pattern: pathPattern, method: nil,
707+
exact: true,
705708
middleware: middleware
706709
+ asyncMiddleware.map(async)
707710
+ [ async(asyncFinal) ]))

Sources/express/RoutePattern.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Noze.io / Macro / ExExpress
44
//
55
// Created by Helge Heß on 6/2/16.
6-
// Copyright © 2016-2021 ZeeZide GmbH. All rights reserved.
6+
// Copyright © 2016-2026 ZeeZide GmbH. All rights reserved.
77
//
88

99
import enum MacroCore.process
@@ -112,6 +112,7 @@ public enum RoutePattern: Hashable {
112112
*/
113113
static func match(pattern p: [ RoutePattern ],
114114
against escapedPathComponents: [ String ],
115+
exact: Bool = false,
115116
variables: inout IncomingMessage.Params) -> String?
116117
{
117118
// Note: Express does a prefix match, which is important for mounting.
@@ -201,7 +202,7 @@ public enum RoutePattern: Hashable {
201202
}
202203

203204
if escapedPathComponents.count > pattern.count {
204-
//if !lastWasWildcard { return nil }
205+
if exact && !lastWasWildcard { return nil }
205206
if lastWasEOL { return nil } // all should have been consumed
206207
}
207208

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import XCTest
2+
import MacroTestUtilities
3+
import class http.IncomingMessage
4+
import enum NIOHTTP1.HTTPMethod
5+
@testable import express
6+
7+
final class ExactMatchTests: XCTestCase {
8+
9+
// MARK: - Method routes do exact matching
10+
11+
func testGetRootDoesNotMatchSubpath() throws {
12+
let route = Route(id: "root")
13+
var didCallRoute = false
14+
route.get("/") { _, _, _ in didCallRoute = true }
15+
16+
let req = IncomingMessage(method: .GET, url: "/foo")
17+
let res = TestServerResponse()
18+
19+
var didCallNext = false
20+
try route.handle(request: req, response: res) { ( args : Any... ) in
21+
didCallNext = true
22+
}
23+
XCTAssertFalse(didCallRoute, "should not match /foo")
24+
XCTAssertTrue(didCallNext, "should call next")
25+
}
26+
27+
func testGetRootMatchesExactRoot() throws {
28+
let route = Route(id: "root")
29+
var didCallRoute = false
30+
route.get("/") { _, _, _ in didCallRoute = true }
31+
32+
let req = IncomingMessage(method: .GET, url: "/")
33+
let res = TestServerResponse()
34+
35+
var didCallNext = false
36+
try route.handle(request: req, response: res) { ( args : Any... ) in
37+
didCallNext = true
38+
}
39+
XCTAssertTrue(didCallRoute, "should match /")
40+
XCTAssertFalse(didCallNext, "should not call next")
41+
}
42+
43+
// MARK: - use() still does prefix matching
44+
45+
func testUseRootMatchesSubpath() throws {
46+
let route = Route(id: "root")
47+
var didCallRoute = false
48+
route.use("/") { _, _, _ in didCallRoute = true }
49+
50+
let req = IncomingMessage(method: .GET, url: "/foo")
51+
let res = TestServerResponse()
52+
53+
try route.handle(request: req, response: res) { ( args : Any... ) in }
54+
XCTAssertTrue(didCallRoute, "use('/') should match /foo (prefix)")
55+
}
56+
57+
// MARK: - all() does exact matching
58+
59+
func testAllRootDoesNotMatchSubpath() throws {
60+
let route = Route(id: "root")
61+
var didCallRoute = false
62+
route.all("/") { _, _, _ in didCallRoute = true }
63+
64+
let req = IncomingMessage(method: .POST, url: "/foo")
65+
let res = TestServerResponse()
66+
67+
var didCallNext = false
68+
try route.handle(request: req, response: res) { ( args : Any... ) in
69+
didCallNext = true
70+
}
71+
XCTAssertFalse(didCallRoute, "all('/') should not match /foo")
72+
XCTAssertTrue(didCallNext, "should call next")
73+
}
74+
75+
func testAllRootMatchesExactRoot() throws {
76+
let route = Route(id: "root")
77+
var didCallRoute = false
78+
route.all("/") { _, _, _ in didCallRoute = true }
79+
80+
let req = IncomingMessage(method: .POST, url: "/")
81+
let res = TestServerResponse()
82+
83+
var didCallNext = false
84+
try route.handle(request: req, response: res) { ( args : Any... ) in
85+
didCallNext = true
86+
}
87+
XCTAssertTrue(didCallRoute, "all('/') should match /")
88+
XCTAssertFalse(didCallNext, "should not call next")
89+
}
90+
91+
func testAllMatchesAnyMethod() throws {
92+
let route = Route(id: "root")
93+
var callCount = 0
94+
route.all("/api") { _, _, _ in callCount += 1 }
95+
96+
for method: HTTPMethod in [ .GET, .POST, .PUT, .DELETE ] {
97+
let req = IncomingMessage(method: method, url: "/api")
98+
let res = TestServerResponse()
99+
try route.handle(request: req, response: res) { _ in }
100+
}
101+
XCTAssertEqual(callCount, 4, "all should match every method")
102+
}
103+
104+
func testAllWithWildcardMatchesSubpath() throws {
105+
let route = Route(id: "root")
106+
var didCallRoute = false
107+
route.all("/api/*") { _, _, _ in didCallRoute = true }
108+
109+
let req = IncomingMessage(method: .GET, url: "/api/users/42")
110+
let res = TestServerResponse()
111+
112+
try route.handle(request: req, response: res) { _ in }
113+
XCTAssertTrue(didCallRoute, "all with wildcard should match subpaths")
114+
}
115+
116+
// MARK: - Wildcard still allows extra segments
117+
118+
func testGetWildcardMatchesDeepPath() throws {
119+
let route = Route(id: "root")
120+
var didCallRoute = false
121+
route.get("/todos/*") { _, _, _ in didCallRoute = true }
122+
123+
let req = IncomingMessage(method: .GET, url: "/todos/1/details")
124+
let res = TestServerResponse()
125+
126+
try route.handle(request: req, response: res) { ( args : Any... ) in }
127+
XCTAssertTrue(didCallRoute, "wildcard should match extra segments")
128+
}
129+
130+
// MARK: - Variable patterns exact match
131+
132+
func testGetWithVariableDoesNotMatchExtra() throws {
133+
let route = Route(id: "root")
134+
var didCallRoute = false
135+
route.get("/users/:id") { _, _, _ in didCallRoute = true }
136+
137+
let req = IncomingMessage(method: .GET, url: "/users/42/profile")
138+
let res = TestServerResponse()
139+
140+
var didCallNext = false
141+
try route.handle(request: req, response: res) { ( args : Any... ) in
142+
didCallNext = true
143+
}
144+
XCTAssertFalse(didCallRoute, "should not match extra /profile")
145+
XCTAssertTrue(didCallNext, "should call next")
146+
}
147+
148+
func testGetWithVariableMatchesExact() throws {
149+
let route = Route(id: "root")
150+
var didCallRoute = false
151+
route.get("/users/:id") { _, _, _ in didCallRoute = true }
152+
153+
let req = IncomingMessage(method: .GET, url: "/users/42")
154+
let res = TestServerResponse()
155+
156+
try route.handle(request: req, response: res) { ( args : Any... ) in }
157+
XCTAssertTrue(didCallRoute, "should match /users/42")
158+
}
159+
160+
// MARK: - Mounted routes with exact matching
161+
162+
func testMountedGetExactMatch() throws {
163+
let outerRoute = Route(id: "outer")
164+
var didCallRoute = false
165+
outerRoute.route("/admin")
166+
.get("/view") { _, _, _ in didCallRoute = true }
167+
168+
let req = IncomingMessage(method: .GET, url: "/admin/view/extra")
169+
let res = TestServerResponse()
170+
171+
var didCallNext = false
172+
try outerRoute.handle(request: req, response: res) { ( args : Any... ) in
173+
didCallNext = true
174+
}
175+
XCTAssertFalse(didCallRoute, "should not match /admin/view/extra")
176+
XCTAssertTrue(didCallNext, "should call next")
177+
}
178+
179+
static var allTests = [
180+
( "testGetRootDoesNotMatchSubpath", testGetRootDoesNotMatchSubpath ),
181+
( "testGetRootMatchesExactRoot", testGetRootMatchesExactRoot ),
182+
( "testUseRootMatchesSubpath", testUseRootMatchesSubpath ),
183+
( "testAllRootDoesNotMatchSubpath", testAllRootDoesNotMatchSubpath ),
184+
( "testAllRootMatchesExactRoot", testAllRootMatchesExactRoot ),
185+
( "testAllMatchesAnyMethod", testAllMatchesAnyMethod ),
186+
( "testAllWithWildcardMatchesSubpath", testAllWithWildcardMatchesSubpath),
187+
( "testGetWildcardMatchesDeepPath", testGetWildcardMatchesDeepPath ),
188+
( "testGetWithVariableMatchesExact", testGetWithVariableMatchesExact ),
189+
( "testMountedGetExactMatch", testMountedGetExactMatch ),
190+
( "testGetWithVariableDoesNotMatchExtra",
191+
testGetWithVariableDoesNotMatchExtra )
192+
]
193+
}

Tests/RouteTests/XCTestManifests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ public func allTests() -> [ XCTestCaseEntry ] {
66
testCase(AsyncMiddlewareTests .allTests),
77
testCase(ErrorMiddlewareTests.allTests),
88
testCase(SimpleRouteTests .allTests),
9-
testCase(RouteMountingTests .allTests)
9+
testCase(RouteMountingTests .allTests),
10+
testCase(ExactMatchTests .allTests)
1011
]
1112
}
1213
#endif

0 commit comments

Comments
 (0)