Skip to content

Commit

Permalink
Test coverage for router
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed Oct 6, 2021
1 parent f5fb9d8 commit be2fa54
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 123 deletions.
16 changes: 8 additions & 8 deletions Sources/Alchemy/Commands/Serve/HTTPHandler.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import NIO
import NIOHTTP1

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

/// The responder to all requests.
private let router: HTTPRouter
private let handler: RequestHandler

/// Initialize with a responder to handle all requests.
/// Initialize with a handler to respond to all requests.
///
/// - Parameter responder: The object to respond to all incoming
/// - Parameter handler: The object to respond to all incoming
/// `Request`s.
init(router: HTTPRouter) {
self.router = router
init(handler: RequestHandler) {
self.handler = handler
}

/// Received incoming `InboundIn` data, writing a response based
Expand Down Expand Up @@ -80,7 +80,7 @@ final class HTTPHandler: ChannelInboundHandler {
// Writes the response when done
writeResponse(
version: request.head.version,
getResponse: { await self.router.handle(request: request) },
getResponse: { await self.handler.handle(request: request) },
to: context
)
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Alchemy/Commands/Serve/RunServe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,20 @@ extension Channel {
channel.pipeline
.addHandlers([
HTTP2FramePayloadToHTTP1ServerCodec(),
HTTPHandler(router: Router.default)
HTTPHandler(handler: Router.default)
])
})
.map { _ in }
},
http1ChannelConfigurator: { http1Channel in
http1Channel.pipeline
.configureHTTPServerPipeline(withErrorHandling: true)
.flatMap { self.pipeline.addHandler(HTTPHandler(router: Router.default)) }
.flatMap { self.pipeline.addHandler(HTTPHandler(handler: Router.default)) }
}
).get()
} else {
try await pipeline.configureHTTPServerPipeline(withErrorHandling: true).get()
try await pipeline.addHandler(HTTPHandler(router: Router.default))
try await pipeline.addHandler(HTTPHandler(handler: Router.default))
}
}
}
25 changes: 9 additions & 16 deletions Sources/Alchemy/Routing/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fileprivate let kRouterPathParameterEscape = ":"
/// An `Router` responds to HTTP requests from the client.
/// Specifically, it takes an `Request` and routes it to
/// a handler that returns an `ResponseConvertible`.
public final class Router: HTTPRouter, Service {
public final class Router: RequestHandler, Service {
/// A route handler. Takes a request and returns a response.
public typealias Handler = (Request) async throws -> ResponseConvertible

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

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

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

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

// Find a matching handler
if let match = trie.search(path: request.path.tokenized, storageKey: request.method) {
if let match = trie.search(path: request.path.tokenized(with: request.method)) {
request.pathParameters = match.parameters
handler = match.value
}
Expand Down Expand Up @@ -122,22 +121,16 @@ public final class Router: HTTPRouter, Service {
} catch {
return await self.internalErrorHandler(req, error)
}
} else {
return await self.internalErrorHandler(req, error)
}

return await self.internalErrorHandler(req, error)
}
}
}
}

private extension String {
var tokenized: [String] {
return split(separator: "/").map(String.init)
}
}

extension HTTPMethod: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.rawValue)
func tokenized(with method: HTTPMethod) -> [String] {
split(separator: "/").map(String.init) + [method.rawValue]
}
}
61 changes: 29 additions & 32 deletions Sources/Alchemy/Routing/Trie.swift
Original file line number Diff line number Diff line change
@@ -1,65 +1,62 @@
/// A trie that stores objects at each node. Supports wildcard path
/// elements denoted by a ":" at the beginning.
final class Trie<Key: Hashable, Value> {
/// Storage of the objects at this node.
private var storage: [Key: Value] = [:]
final class Trie<Value> {
/// Storage of the object at this node.
private var value: Value?
/// This node's children, mapped by their path for instant lookup.
private var children: [String: Trie] = [:]
/// Any children with wildcards in their path.
private var wildcardChildren: [String: Trie] = [:]
/// Any children with parameters in their path.
private var parameterChildren: [String: Trie] = [:]

/// Search this node & it's children for an object at a path,
/// stored with the given key.
/// Search this node & it's children for an object at a path.
///
/// - Parameters:
/// - path: The path of the object to search for. If this is
/// empty, it is assumed the object can only be at this node.
/// - storageKey: The key by which the object is stored.
/// - Parameter path: The path of the object to search for. If this is
/// empty, it is assumed the object can only be at this node.
/// - Returns: A tuple containing the object and any parsed path
/// parameters. `nil` if the object isn't in this node or its
/// children.
func search(path: [String], storageKey: Key) -> (value: Value, parameters: [PathParameter])? {
func search(path: [String]) -> (value: Value, parameters: [PathParameter])? {
if let first = path.first {
let newPath = Array(path.dropFirst())
if let matchingChild = children[first] {
return matchingChild.search(path: newPath, storageKey: storageKey)
} else {
for (wildcard, node) in wildcardChildren {
guard var val = node.search(path: newPath, storageKey: storageKey) else {
continue
}

val.parameters.insert(PathParameter(parameter: wildcard, stringValue: first), at: 0)
return val
return matchingChild.search(path: newPath)
}

for (wildcard, node) in parameterChildren {
guard var val = node.search(path: newPath) else {
continue
}
return nil

val.parameters.insert(PathParameter(parameter: wildcard, stringValue: first), at: 0)
return val
}
} else {
return storage[storageKey].map { ($0, []) }
return nil
}

return value.map { ($0, []) }
}

/// Inserts a value at the given path with a storage key.
/// Inserts a value at the given path.
///
/// - Parameters:
/// - path: The path to the node where this value should be
/// stored.
/// - storageKey: The key by which to store the value.
/// - value: The value to store.
func insert(path: [String], storageKey: Key, value: Value) {
func insert(path: [String], value: Value) {
if let first = path.first {
if first.hasPrefix(":") {
let firstWithoutEscape = String(first.dropFirst())
let child = wildcardChildren[firstWithoutEscape] ?? Self()
child.insert(path: Array(path.dropFirst()), storageKey: storageKey, value: value)
wildcardChildren[firstWithoutEscape] = child
let child = parameterChildren[firstWithoutEscape] ?? Self()
child.insert(path: Array(path.dropFirst()), value: value)
parameterChildren[firstWithoutEscape] = child
} else {
let child = children[first] ?? Self()
child.insert(path: Array(path.dropFirst()), storageKey: storageKey, value: value)
child.insert(path: Array(path.dropFirst()), value: value)
children[first] = child
}
} else {
storage[storageKey] = value
self.value = value
}
}
}
Loading

0 comments on commit be2fa54

Please sign in to comment.