Skip to content

Commit be2fa54

Browse files
Test coverage for router
1 parent f5fb9d8 commit be2fa54

File tree

6 files changed

+163
-123
lines changed

6 files changed

+163
-123
lines changed

Sources/Alchemy/Commands/Serve/HTTPHandler.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import NIO
22
import NIOHTTP1
33

4-
/// A type that can respond to HTTP requests.
5-
protocol HTTPRouter {
4+
/// A type that can handle HTTP requests.
5+
protocol RequestHandler {
66
/// Given a `Request`, return a `Response`. Should never result in
77
/// an error.
88
///
@@ -25,14 +25,14 @@ final class HTTPHandler: ChannelInboundHandler {
2525
private var request: Request?
2626

2727
/// The responder to all requests.
28-
private let router: HTTPRouter
28+
private let handler: RequestHandler
2929

30-
/// Initialize with a responder to handle all requests.
30+
/// Initialize with a handler to respond to all requests.
3131
///
32-
/// - Parameter responder: The object to respond to all incoming
32+
/// - Parameter handler: The object to respond to all incoming
3333
/// `Request`s.
34-
init(router: HTTPRouter) {
35-
self.router = router
34+
init(handler: RequestHandler) {
35+
self.handler = handler
3636
}
3737

3838
/// Received incoming `InboundIn` data, writing a response based
@@ -80,7 +80,7 @@ final class HTTPHandler: ChannelInboundHandler {
8080
// Writes the response when done
8181
writeResponse(
8282
version: request.head.version,
83-
getResponse: { await self.router.handle(request: request) },
83+
getResponse: { await self.handler.handle(request: request) },
8484
to: context
8585
)
8686
}

Sources/Alchemy/Commands/Serve/RunServe.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,20 @@ extension Channel {
162162
channel.pipeline
163163
.addHandlers([
164164
HTTP2FramePayloadToHTTP1ServerCodec(),
165-
HTTPHandler(router: Router.default)
165+
HTTPHandler(handler: Router.default)
166166
])
167167
})
168168
.map { _ in }
169169
},
170170
http1ChannelConfigurator: { http1Channel in
171171
http1Channel.pipeline
172172
.configureHTTPServerPipeline(withErrorHandling: true)
173-
.flatMap { self.pipeline.addHandler(HTTPHandler(router: Router.default)) }
173+
.flatMap { self.pipeline.addHandler(HTTPHandler(handler: Router.default)) }
174174
}
175175
).get()
176176
} else {
177177
try await pipeline.configureHTTPServerPipeline(withErrorHandling: true).get()
178-
try await pipeline.addHandler(HTTPHandler(router: Router.default))
178+
try await pipeline.addHandler(HTTPHandler(handler: Router.default))
179179
}
180180
}
181181
}

Sources/Alchemy/Routing/Router.swift

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fileprivate let kRouterPathParameterEscape = ":"
1010
/// An `Router` responds to HTTP requests from the client.
1111
/// Specifically, it takes an `Request` and routes it to
1212
/// a handler that returns an `ResponseConvertible`.
13-
public final class Router: HTTPRouter, Service {
13+
public final class Router: RequestHandler, Service {
1414
/// A route handler. Takes a request and returns a response.
1515
public typealias Handler = (Request) async throws -> ResponseConvertible
1616

@@ -51,7 +51,7 @@ public final class Router: HTTPRouter, Service {
5151
var pathPrefixes: [String] = []
5252

5353
/// A trie that holds all the handlers.
54-
private let trie = Trie<HTTPMethod, HTTPHandler>()
54+
private let trie = Trie<HTTPHandler>()
5555

5656
/// Creates a new router.
5757
init() {}
@@ -65,10 +65,9 @@ public final class Router: HTTPRouter, Service {
6565
/// - method: The method of a request this handler expects.
6666
/// - path: The path of a requst this handler can handle.
6767
func add(handler: @escaping Handler, for method: HTTPMethod, path: String) {
68-
let pathPrefixes = pathPrefixes.map { $0.hasPrefix("/") ? String($0.dropFirst()) : $0 }
69-
let splitPath = pathPrefixes + path.tokenized
68+
let splitPath = pathPrefixes + path.tokenized(with: method)
7069
let middlewareClosures = middlewares.reversed().map(Middleware.intercept)
71-
trie.insert(path: splitPath, storageKey: method) {
70+
trie.insert(path: splitPath) {
7271
var next = self.cleanHandler(handler)
7372

7473
for middleware in middlewareClosures {
@@ -93,7 +92,7 @@ public final class Router: HTTPRouter, Service {
9392
var handler = cleanHandler(notFoundHandler)
9493

9594
// Find a matching handler
96-
if let match = trie.search(path: request.path.tokenized, storageKey: request.method) {
95+
if let match = trie.search(path: request.path.tokenized(with: request.method)) {
9796
request.pathParameters = match.parameters
9897
handler = match.value
9998
}
@@ -122,22 +121,16 @@ public final class Router: HTTPRouter, Service {
122121
} catch {
123122
return await self.internalErrorHandler(req, error)
124123
}
125-
} else {
126-
return await self.internalErrorHandler(req, error)
127124
}
125+
126+
return await self.internalErrorHandler(req, error)
128127
}
129128
}
130129
}
131130
}
132131

133132
private extension String {
134-
var tokenized: [String] {
135-
return split(separator: "/").map(String.init)
136-
}
137-
}
138-
139-
extension HTTPMethod: Hashable {
140-
public func hash(into hasher: inout Hasher) {
141-
hasher.combine(self.rawValue)
133+
func tokenized(with method: HTTPMethod) -> [String] {
134+
split(separator: "/").map(String.init) + [method.rawValue]
142135
}
143136
}

Sources/Alchemy/Routing/Trie.swift

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,62 @@
11
/// A trie that stores objects at each node. Supports wildcard path
22
/// elements denoted by a ":" at the beginning.
3-
final class Trie<Key: Hashable, Value> {
4-
/// Storage of the objects at this node.
5-
private var storage: [Key: Value] = [:]
3+
final class Trie<Value> {
4+
/// Storage of the object at this node.
5+
private var value: Value?
66
/// This node's children, mapped by their path for instant lookup.
77
private var children: [String: Trie] = [:]
8-
/// Any children with wildcards in their path.
9-
private var wildcardChildren: [String: Trie] = [:]
8+
/// Any children with parameters in their path.
9+
private var parameterChildren: [String: Trie] = [:]
1010

11-
/// Search this node & it's children for an object at a path,
12-
/// stored with the given key.
11+
/// Search this node & it's children for an object at a path.
1312
///
14-
/// - Parameters:
15-
/// - path: The path of the object to search for. If this is
16-
/// empty, it is assumed the object can only be at this node.
17-
/// - storageKey: The key by which the object is stored.
13+
/// - Parameter path: The path of the object to search for. If this is
14+
/// empty, it is assumed the object can only be at this node.
1815
/// - Returns: A tuple containing the object and any parsed path
1916
/// parameters. `nil` if the object isn't in this node or its
2017
/// children.
21-
func search(path: [String], storageKey: Key) -> (value: Value, parameters: [PathParameter])? {
18+
func search(path: [String]) -> (value: Value, parameters: [PathParameter])? {
2219
if let first = path.first {
2320
let newPath = Array(path.dropFirst())
2421
if let matchingChild = children[first] {
25-
return matchingChild.search(path: newPath, storageKey: storageKey)
26-
} else {
27-
for (wildcard, node) in wildcardChildren {
28-
guard var val = node.search(path: newPath, storageKey: storageKey) else {
29-
continue
30-
}
31-
32-
val.parameters.insert(PathParameter(parameter: wildcard, stringValue: first), at: 0)
33-
return val
22+
return matchingChild.search(path: newPath)
23+
}
24+
25+
for (wildcard, node) in parameterChildren {
26+
guard var val = node.search(path: newPath) else {
27+
continue
3428
}
35-
return nil
29+
30+
val.parameters.insert(PathParameter(parameter: wildcard, stringValue: first), at: 0)
31+
return val
3632
}
37-
} else {
38-
return storage[storageKey].map { ($0, []) }
33+
34+
return nil
3935
}
36+
37+
return value.map { ($0, []) }
4038
}
4139

42-
/// Inserts a value at the given path with a storage key.
40+
/// Inserts a value at the given path.
4341
///
4442
/// - Parameters:
4543
/// - path: The path to the node where this value should be
4644
/// stored.
47-
/// - storageKey: The key by which to store the value.
4845
/// - value: The value to store.
49-
func insert(path: [String], storageKey: Key, value: Value) {
46+
func insert(path: [String], value: Value) {
5047
if let first = path.first {
5148
if first.hasPrefix(":") {
5249
let firstWithoutEscape = String(first.dropFirst())
53-
let child = wildcardChildren[firstWithoutEscape] ?? Self()
54-
child.insert(path: Array(path.dropFirst()), storageKey: storageKey, value: value)
55-
wildcardChildren[firstWithoutEscape] = child
50+
let child = parameterChildren[firstWithoutEscape] ?? Self()
51+
child.insert(path: Array(path.dropFirst()), value: value)
52+
parameterChildren[firstWithoutEscape] = child
5653
} else {
5754
let child = children[first] ?? Self()
58-
child.insert(path: Array(path.dropFirst()), storageKey: storageKey, value: value)
55+
child.insert(path: Array(path.dropFirst()), value: value)
5956
children[first] = child
6057
}
6158
} else {
62-
storage[storageKey] = value
59+
self.value = value
6360
}
6461
}
6562
}

0 commit comments

Comments
 (0)