From 4d866d151433da82ae12292298e47692cf8eac4e Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Wed, 9 Jun 2021 15:30:57 +0100 Subject: [PATCH 01/15] Moved middleware into an EventLoop. --- .../Middleware/ClosureMiddleware.swift | 2 +- Shock/Classes/Middleware/Middleware.swift | 28 +++++---- .../Middleware/MockRoutesMiddleware.swift | 2 +- Shock/Classes/MockServer.swift | 16 ++--- Shock/Classes/NIO/MockNIOHTTPHandler.swift | 60 ++++++++++++++----- Shock/Classes/NIO/MockNIOHTTPServer.swift | 28 +++++---- 6 files changed, 88 insertions(+), 48 deletions(-) diff --git a/Shock/Classes/Middleware/ClosureMiddleware.swift b/Shock/Classes/Middleware/ClosureMiddleware.swift index d57a01b..a5563ee 100644 --- a/Shock/Classes/Middleware/ClosureMiddleware.swift +++ b/Shock/Classes/Middleware/ClosureMiddleware.swift @@ -7,7 +7,7 @@ import Foundation -public class ClosureMiddleware: Middleware { +public struct ClosureMiddleware: Middleware { public typealias Closure = (_ request: MiddlewareRequestContext, _ response: MiddlewareResponseContext, diff --git a/Shock/Classes/Middleware/Middleware.swift b/Shock/Classes/Middleware/Middleware.swift index 73d418c..e4bfc2e 100644 --- a/Shock/Classes/Middleware/Middleware.swift +++ b/Shock/Classes/Middleware/Middleware.swift @@ -5,7 +5,7 @@ // Created by Jack Newcombe on 01/10/2020. // -import Foundation +import NIO public typealias HandlerClosure = (MiddlewareRequestContext, MiddlewareResponseContext) -> Void @@ -71,14 +71,24 @@ class MiddlewareService { let next: () -> Void } - private(set) var middleware: [Middleware] = [] - + private let middleware: [Middleware] + private let notFoundHandler: HandlerClosure? private var context: MiddlewareContext? + + public init(middleware: [Middleware], notFoundHandler: HandlerClosure?) { + self.middleware = middleware + self.notFoundHandler = notFoundHandler + } - var notFoundHandler: HandlerClosure? - - func executeAll(forRequest request: MockNIOHTTPRequest) -> MiddlewareContext? { - executeAll(forRequest: request, middleware: middleware) + func executeAll(forRequest request: MockNIOHTTPRequest) -> EventLoopFuture { + let promise = request.eventLoop.makePromise(of: MiddlewareContext?.self) + + request.eventLoop.execute { + let context = self.executeAll(forRequest: request, middleware: self.middleware) + promise.succeed(context) + } + + return promise.futureResult } private func executeAll(forRequest request: MockNIOHTTPRequest, middleware: [Middleware]) -> MiddlewareContext? { @@ -99,8 +109,4 @@ class MiddlewareService { return context } - func add(middleware mdl: Middleware) { - middleware.append(mdl) - } - } diff --git a/Shock/Classes/Middleware/MockRoutesMiddleware.swift b/Shock/Classes/Middleware/MockRoutesMiddleware.swift index e67be13..42009c7 100644 --- a/Shock/Classes/Middleware/MockRoutesMiddleware.swift +++ b/Shock/Classes/Middleware/MockRoutesMiddleware.swift @@ -7,7 +7,7 @@ import Foundation -class MockRoutesMiddleware: Middleware { +struct MockRoutesMiddleware: Middleware { let router: MockNIOHTTPRouter diff --git a/Shock/Classes/MockServer.swift b/Shock/Classes/MockServer.swift index addbd5a..bbc3e8f 100644 --- a/Shock/Classes/MockServer.swift +++ b/Shock/Classes/MockServer.swift @@ -17,17 +17,10 @@ public class MockServer { /// The range in which to find a free port on which to launch the server private let portRange: ClosedRange - private var httpServer = MockNIOHttpServer() + private var httpServer: MockNIOHttpServer private var socketServer: MockNIOSocketServer? - + private let router: MockNIOHTTPRouter private let responseFactory: ResponseFactory - - private lazy var middleware: MockRoutesMiddleware = { - let middleware = MockRoutesMiddleware(router: MockNIOHTTPRouter(), - responseFactory: responseFactory) - httpServer.add(middleware: middleware) - return middleware - }() public var selectedHTTPPort = 0 public var selectedSocketPort = 0 @@ -41,6 +34,9 @@ public class MockServer { public init(portRange: ClosedRange, bundle: Bundle = Bundle.main) { self.portRange = portRange self.responseFactory = ResponseFactory(bundle: bundle) + self.router = MockNIOHTTPRouter() + self.httpServer = MockNIOHttpServer(responseFactory: self.responseFactory, + router: self.router) } // MARK: Server managements @@ -122,7 +118,7 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") return } - middleware.router.register(route: route) { request, response in + router.register(route: route) { request, response in switch route { case .redirect(_, let destination): diff --git a/Shock/Classes/NIO/MockNIOHTTPHandler.swift b/Shock/Classes/NIO/MockNIOHTTPHandler.swift index 7104b0b..5f09708 100644 --- a/Shock/Classes/NIO/MockNIOHTTPHandler.swift +++ b/Shock/Classes/NIO/MockNIOHTTPHandler.swift @@ -14,14 +14,16 @@ class MockNIOHTTPHandler { typealias OutboundOut = HTTPServerResponsePart private let router: MockNIOHTTPRouter - private let middlewareService: MiddlewareService - private var httpRequest: HTTPRequestHead? private var handlerRequest: MockNIOHTTPRequest? - init(router: MockNIOHTTPRouter, middlewareService: MiddlewareService) { + var middleware: [Middleware] + var notFoundHandler: HandlerClosure? + + init(router: MockNIOHTTPRouter, middleware: [Middleware], notFoundHandler: HandlerClosure?) { self.router = router - self.middlewareService = middlewareService + self.middleware = middleware + self.notFoundHandler = notFoundHandler } private func httpResponseHeadForRequestHead(_ request: HTTPRequestHead, status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) -> HTTPResponseHead { @@ -51,7 +53,7 @@ class MockNIOHTTPHandler { } } - private func requestForHTTPRequestHead(_ request: HTTPRequestHead) -> MockNIOHTTPRequest? { + private func requestForHTTPRequestHead(_ request: HTTPRequestHead, eventLoop: EventLoop) -> MockNIOHTTPRequest? { guard let url = URLComponents(string: request.uri) else { return nil } let path = url.path let method = stringForHTTPMethod(request.method) @@ -65,13 +67,14 @@ class MockNIOHTTPHandler { queryParams = queryItems.reduce(into: [(String, String)](), { $0.append(($1.name, $1.value ?? "")) }) } - return MockNIOHTTPRequest(path: path, - queryParams: queryParams, - method: method, - headers: headers, - body: body, - address: address, - params: params) + return MockNIOHTTPRequest(eventLoop: eventLoop, + path: path, + queryParams: queryParams, + method: method, + headers: headers, + body: body, + address: address, + params: params) } private func handleResponse(forResponseContext middlewareContext: MiddlewareContext, in channelHandlerContext: ChannelHandlerContext) { @@ -119,7 +122,7 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { switch reqPart { case .head(let request): self.httpRequest = request - self.handlerRequest = requestForHTTPRequestHead(request) + self.handlerRequest = requestForHTTPRequestHead(request, eventLoop: context.eventLoop) case .body(buffer: var bytes): guard var handlerRequest = self.handlerRequest else { return } handlerRequest.body += bytes.readBytes(length: bytes.readableBytes) ?? [] @@ -128,8 +131,11 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { guard let request = self.httpRequest else { return } guard let handlerRequest = self.handlerRequest else { return } - if let finalContext = middlewareService.executeAll(forRequest: handlerRequest) { - handleResponse(forResponseContext: finalContext, in: context) + let responder = MiddlwareResponder(middleware: middleware, notFoundHandler: notFoundHandler) + responder.respond(to: handlerRequest).whenSuccess { (responseContext) in + if let finalContext = responseContext { + self.handleResponse(forResponseContext: finalContext, in: context) + } } self.httpRequest = nil @@ -137,3 +143,27 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { } } } + +// MARK: MiddlewareResponder + +struct MiddlwareResponder { + + let middleware: [Middleware] + let notFoundHandler: HandlerClosure? + + let middlewareService = ThreadSpecificVariable() + private func makeMiddlewareService(for eventLoop: EventLoop) -> MiddlewareService { + if let existingService = middlewareService.currentValue { + return existingService + } + + let newService = MiddlewareService(middleware: middleware, notFoundHandler: notFoundHandler) + middlewareService.currentValue = newService + return newService + } + + func respond(to request: MockNIOHTTPRequest) -> EventLoopFuture { + let middlewareService = makeMiddlewareService(for: request.eventLoop) + return middlewareService.executeAll(forRequest: request) + } +} diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index 5a83291..f606db9 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -12,21 +12,24 @@ import NIOHTTP1 /// SwiftNIO implementation of mock HTTP server class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { - private let router = MockNIOHTTPRouter() - private let middlewareService = MiddlewareService() + private let responseFactory: ResponseFactory + private let router: MockNIOHTTPRouter + private var middleware = [Middleware]() + private var httpHandler: MockNIOHTTPHandler? var notFoundHandler: HandlerClosure? { get { - middlewareService.notFoundHandler + httpHandler?.notFoundHandler } set { - middlewareService.notFoundHandler = newValue + httpHandler?.notFoundHandler = newValue } } var methodRoutes: [MockHTTPMethod: MockNIOHTTPMethodRoute] = [:] - - override init() { + init(responseFactory: ResponseFactory, router: MockNIOHTTPRouter) { + self.responseFactory = responseFactory + self.router = router methodRoutes[.delete] = MockNIOHTTPMethodRoute(method: "DELETE", router: router) methodRoutes[.patch] = MockNIOHTTPMethodRoute(method: "PATCH", router: router) methodRoutes[.head] = MockNIOHTTPMethodRoute(method: "HEAD", router: router) @@ -39,18 +42,22 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void { try start(port) { (channel) -> EventLoopFuture in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { - channel.pipeline.addHandler(MockNIOHTTPHandler(router: self.router, - middlewareService: self.middlewareService)) + let routeMiddleware = MockRoutesMiddleware(router: self.router, + responseFactory: self.responseFactory) + self.httpHandler = MockNIOHTTPHandler(router: self.router, + middleware: [routeMiddleware] + self.middleware, + notFoundHandler: self.notFoundHandler) + return channel.pipeline.addHandler(self.httpHandler!) } } } func add(middleware: Middleware) { - middlewareService.add(middleware: middleware) + httpHandler?.middleware.append(middleware) } func has(middlewareOfType type: T.Type) -> Bool where T: Middleware { - return middlewareService.middleware.contains { $0 is T } + return (httpHandler?.middleware ?? []).contains { $0 is T } } } @@ -83,6 +90,7 @@ class MockNIOHTTPRouter: MockHttpRouter { } struct MockNIOHTTPRequest: MockHttpRequest { + var eventLoop: EventLoop var path: String var queryParams: [(String, String)] var method: String From 567e905477a09610308e5b53effef4126c0ab6d1 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Thu, 10 Jun 2021 10:24:40 +0100 Subject: [PATCH 02/15] Rejigging and tidy up. --- Shock/Classes/MockHTTPServerProtocols.swift | 17 +------ Shock/Classes/MockServer.swift | 7 +-- Shock/Classes/NIO/MockNIOHTTPHandler.swift | 41 ++++++++++++--- Shock/Classes/NIO/MockNIOHTTPServer.swift | 55 +++++---------------- 4 files changed, 50 insertions(+), 70 deletions(-) diff --git a/Shock/Classes/MockHTTPServerProtocols.swift b/Shock/Classes/MockHTTPServerProtocols.swift index d502aa7..f1a6064 100644 --- a/Shock/Classes/MockHTTPServerProtocols.swift +++ b/Shock/Classes/MockHTTPServerProtocols.swift @@ -8,25 +8,10 @@ import Foundation protocol MockHttpRouter { - func register(route: MockHTTPRoute, handler: HandlerClosure?) -} - -protocol MockMethodRoute { - var method: String { get } - var router: MockHttpRouter { get } -} - -extension MockMethodRoute { - subscript(route: MockHTTPRoute) -> HandlerClosure? { - set { - router.register(route: route, handler: newValue) - } - get { return nil } - } + mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) } protocol MockHttpServer { - var methodRoutes: [MockHTTPMethod: MockNIOHTTPMethodRoute] { get } var notFoundHandler: HandlerClosure? { get set } func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void func stop() diff --git a/Shock/Classes/MockServer.swift b/Shock/Classes/MockServer.swift index bbc3e8f..cc42edf 100644 --- a/Shock/Classes/MockServer.swift +++ b/Shock/Classes/MockServer.swift @@ -19,7 +19,6 @@ public class MockServer { private var httpServer: MockNIOHttpServer private var socketServer: MockNIOSocketServer? - private let router: MockNIOHTTPRouter private let responseFactory: ResponseFactory public var selectedHTTPPort = 0 @@ -34,9 +33,7 @@ public class MockServer { public init(portRange: ClosedRange, bundle: Bundle = Bundle.main) { self.portRange = portRange self.responseFactory = ResponseFactory(bundle: bundle) - self.router = MockNIOHTTPRouter() - self.httpServer = MockNIOHttpServer(responseFactory: self.responseFactory, - router: self.router) + self.httpServer = MockNIOHttpServer(responseFactory: self.responseFactory) } // MARK: Server managements @@ -118,7 +115,7 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") return } - router.register(route: route) { request, response in + httpServer.router?.register(route: route) { request, response in switch route { case .redirect(_, let destination): diff --git a/Shock/Classes/NIO/MockNIOHTTPHandler.swift b/Shock/Classes/NIO/MockNIOHTTPHandler.swift index 5f09708..cbae209 100644 --- a/Shock/Classes/NIO/MockNIOHTTPHandler.swift +++ b/Shock/Classes/NIO/MockNIOHTTPHandler.swift @@ -13,16 +13,18 @@ class MockNIOHTTPHandler { typealias InboundIn = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart - private let router: MockNIOHTTPRouter + private let responseFactory: ResponseFactory private var httpRequest: HTTPRequestHead? private var handlerRequest: MockNIOHTTPRequest? - var middleware: [Middleware] + var router: MockNIOHTTPRouter? + var middleware = [Middleware]() var notFoundHandler: HandlerClosure? - init(router: MockNIOHTTPRouter, middleware: [Middleware], notFoundHandler: HandlerClosure?) { - self.router = router - self.middleware = middleware + init(responseFactory: ResponseFactory, + notFoundHandler: HandlerClosure?) { + self.router = MockNIOHTTPRouter() + self.responseFactory = responseFactory self.notFoundHandler = notFoundHandler } @@ -130,8 +132,12 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { case .end(_): guard let request = self.httpRequest else { return } guard let handlerRequest = self.handlerRequest else { return } + guard let router = self.router else { return } - let responder = MiddlwareResponder(middleware: middleware, notFoundHandler: notFoundHandler) + let routeMiddleware = MockRoutesMiddleware(router: router, + responseFactory: self.responseFactory) + let responder = MiddlwareResponder(middleware: [routeMiddleware] + middleware, + notFoundHandler: notFoundHandler) responder.respond(to: handlerRequest).whenSuccess { (responseContext) in if let finalContext = responseContext { self.handleResponse(forResponseContext: finalContext, in: context) @@ -167,3 +173,26 @@ struct MiddlwareResponder { return middlewareService.executeAll(forRequest: request) } } + +struct MockNIOHTTPRouter: MockHttpRouter { + typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] + private var routes = [String: RouteHandlerMapping]() + + func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { + guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } + let methodRoutes = routes[method] ?? RouteHandlerMapping() + for (candidate, handler) in methodRoutes { + if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { + return handler + } + } + return nil + } + + mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) { + guard let method = route.method?.rawValue else { return } + var methodRoutes = routes[method] ?? RouteHandlerMapping() + methodRoutes[route] = handler + routes[method] = methodRoutes + } +} diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index f606db9..3b6b268 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -13,10 +13,18 @@ import NIOHTTP1 class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { private let responseFactory: ResponseFactory - private let router: MockNIOHTTPRouter - private var middleware = [Middleware]() private var httpHandler: MockNIOHTTPHandler? + var router: MockNIOHTTPRouter? { + get { + httpHandler?.router + } + set { + guard let httpHandler = self.httpHandler else { return } + httpHandler.router = newValue + } + } + var notFoundHandler: HandlerClosure? { get { httpHandler?.notFoundHandler @@ -25,27 +33,16 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { httpHandler?.notFoundHandler = newValue } } - var methodRoutes: [MockHTTPMethod: MockNIOHTTPMethodRoute] = [:] - init(responseFactory: ResponseFactory, router: MockNIOHTTPRouter) { + init(responseFactory: ResponseFactory) { self.responseFactory = responseFactory - self.router = router - methodRoutes[.delete] = MockNIOHTTPMethodRoute(method: "DELETE", router: router) - methodRoutes[.patch] = MockNIOHTTPMethodRoute(method: "PATCH", router: router) - methodRoutes[.head] = MockNIOHTTPMethodRoute(method: "HEAD", router: router) - methodRoutes[.post] = MockNIOHTTPMethodRoute(method: "POST", router: router) - methodRoutes[.get] = MockNIOHTTPMethodRoute(method: "GET", router: router) - methodRoutes[.put] = MockNIOHTTPMethodRoute(method: "PUT", router: router) super.init() } func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void { try start(port) { (channel) -> EventLoopFuture in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { - let routeMiddleware = MockRoutesMiddleware(router: self.router, - responseFactory: self.responseFactory) - self.httpHandler = MockNIOHTTPHandler(router: self.router, - middleware: [routeMiddleware] + self.middleware, + self.httpHandler = MockNIOHTTPHandler(responseFactory: self.responseFactory, notFoundHandler: self.notFoundHandler) return channel.pipeline.addHandler(self.httpHandler!) } @@ -61,34 +58,6 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { } } -struct MockNIOHTTPMethodRoute: MockMethodRoute { - let method: String - let router: MockHttpRouter -} - -class MockNIOHTTPRouter: MockHttpRouter { - typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] - private var routes = [String: RouteHandlerMapping]() - - func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { - guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } - let methodRoutes = routes[method] ?? RouteHandlerMapping() - for (candidate, handler) in methodRoutes { - if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { - return handler - } - } - return nil - } - - func register(route: MockHTTPRoute, handler: HandlerClosure?) { - guard let method = route.method?.rawValue else { return } - var methodRoutes = routes[method] ?? RouteHandlerMapping() - methodRoutes[route] = handler - routes[method] = methodRoutes - } -} - struct MockNIOHTTPRequest: MockHttpRequest { var eventLoop: EventLoop var path: String From 19bf24d56777e8163b2b5c927ae8a3d177a764f3 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Thu, 10 Jun 2021 13:43:58 +0100 Subject: [PATCH 03/15] All green. --- Shock/Classes/MockHTTPServerProtocols.swift | 2 + Shock/Classes/MockServer.swift | 2 +- Shock/Classes/NIO/MockNIOHTTPHandler.swift | 51 +++++++++++++-------- Shock/Classes/NIO/MockNIOHTTPServer.swift | 33 +++++-------- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/Shock/Classes/MockHTTPServerProtocols.swift b/Shock/Classes/MockHTTPServerProtocols.swift index f1a6064..cdccbed 100644 --- a/Shock/Classes/MockHTTPServerProtocols.swift +++ b/Shock/Classes/MockHTTPServerProtocols.swift @@ -8,11 +8,13 @@ import Foundation protocol MockHttpRouter { + var requiresRouteMiddleware: Bool { get } mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) } protocol MockHttpServer { var notFoundHandler: HandlerClosure? { get set } + func register(route: MockHTTPRoute, handler: HandlerClosure?) func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void func stop() } diff --git a/Shock/Classes/MockServer.swift b/Shock/Classes/MockServer.swift index cc42edf..9bab6a4 100644 --- a/Shock/Classes/MockServer.swift +++ b/Shock/Classes/MockServer.swift @@ -115,7 +115,7 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") return } - httpServer.router?.register(route: route) { request, response in + httpServer.register(route: route) { request, response in switch route { case .redirect(_, let destination): diff --git a/Shock/Classes/NIO/MockNIOHTTPHandler.swift b/Shock/Classes/NIO/MockNIOHTTPHandler.swift index cbae209..0029118 100644 --- a/Shock/Classes/NIO/MockNIOHTTPHandler.swift +++ b/Shock/Classes/NIO/MockNIOHTTPHandler.swift @@ -13,23 +13,25 @@ class MockNIOHTTPHandler { typealias InboundIn = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart - private let responseFactory: ResponseFactory private var httpRequest: HTTPRequestHead? private var handlerRequest: MockNIOHTTPRequest? - - var router: MockNIOHTTPRouter? - var middleware = [Middleware]() - var notFoundHandler: HandlerClosure? + private var responseFactory: ResponseFactory + private var router: MockNIOHTTPRouter + private var middleware: [Middleware] + private var notFoundHandler: HandlerClosure? init(responseFactory: ResponseFactory, + router: MockNIOHTTPRouter, + middleware: [Middleware], notFoundHandler: HandlerClosure?) { - self.router = MockNIOHTTPRouter() self.responseFactory = responseFactory + self.router = router + self.middleware = middleware self.notFoundHandler = notFoundHandler } - private func httpResponseHeadForRequestHead(_ request: HTTPRequestHead, status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) -> HTTPResponseHead { - HTTPResponseHead(version: request.version, status: status, headers: headers) + private func httpResponseHeadForVersion(_ version: HTTPVersion, status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) -> HTTPResponseHead { + HTTPResponseHead(version: version, status: status, headers: headers) } private func completeResponse(_ context: ChannelHandlerContext, trailers: HTTPHeaders?) { @@ -79,7 +81,9 @@ class MockNIOHTTPHandler { params: params) } - private func handleResponse(forResponseContext middlewareContext: MiddlewareContext, in channelHandlerContext: ChannelHandlerContext) { + private func handleResponse(forResponseContext middlewareContext: MiddlewareContext, + in channelHandlerContext: ChannelHandlerContext, + version: HTTPVersion) { // TODO @@ -88,10 +92,9 @@ class MockNIOHTTPHandler { let statusCode = middlewareContext.responseContext.statusCode // Write head - guard let requestHead = self.httpRequest else { return } - let responseHead = httpResponseHeadForRequestHead(requestHead, - status: HTTPResponseStatus(statusCode: statusCode), - headers: HTTPHeaders(headers.map { ($0.key, $0.value) })) + let responseHead = httpResponseHeadForVersion(version, + status: HTTPResponseStatus(statusCode: statusCode), + headers: HTTPHeaders(headers.map { ($0.key, $0.value) })) let outboundHeadData = self.wrapOutboundOut(.head(responseHead)) channelHandlerContext.writeAndFlush(outboundHeadData, promise: nil) @@ -105,8 +108,8 @@ class MockNIOHTTPHandler { completeResponse(channelHandlerContext, trailers: nil) } - private func writeAndFlushHeaderResponse(status: HTTPResponseStatus, for request: HTTPRequestHead, in context: ChannelHandlerContext) { - _ = context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHeadForRequestHead(request, status: status)))) + private func writeAndFlushHeaderResponse(status: HTTPResponseStatus, in context: ChannelHandlerContext, version: HTTPVersion) { + _ = context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHeadForVersion(version, status: status)))) } } @@ -132,15 +135,19 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { case .end(_): guard let request = self.httpRequest else { return } guard let handlerRequest = self.handlerRequest else { return } - guard let router = self.router else { return } + guard let version = self.httpRequest?.version else { return } - let routeMiddleware = MockRoutesMiddleware(router: router, - responseFactory: self.responseFactory) - let responder = MiddlwareResponder(middleware: [routeMiddleware] + middleware, + var finalMiddleware = middleware + if router.requiresRouteMiddleware { + let routeMiddleware = MockRoutesMiddleware(router: self.router, + responseFactory: self.responseFactory) + finalMiddleware.append(routeMiddleware) + } + let responder = MiddlwareResponder(middleware: finalMiddleware, notFoundHandler: notFoundHandler) responder.respond(to: handlerRequest).whenSuccess { (responseContext) in if let finalContext = responseContext { - self.handleResponse(forResponseContext: finalContext, in: context) + self.handleResponse(forResponseContext: finalContext, in: context, version: version) } } @@ -178,6 +185,10 @@ struct MockNIOHTTPRouter: MockHttpRouter { typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] private var routes = [String: RouteHandlerMapping]() + var requiresRouteMiddleware: Bool { + !routes.isEmpty + } + func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } let methodRoutes = routes[method] ?? RouteHandlerMapping() diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index 3b6b268..be3ca46 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -14,25 +14,10 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { private let responseFactory: ResponseFactory private var httpHandler: MockNIOHTTPHandler? - - var router: MockNIOHTTPRouter? { - get { - httpHandler?.router - } - set { - guard let httpHandler = self.httpHandler else { return } - httpHandler.router = newValue - } - } - - var notFoundHandler: HandlerClosure? { - get { - httpHandler?.notFoundHandler - } - set { - httpHandler?.notFoundHandler = newValue - } - } + private var router = MockNIOHTTPRouter() + private var middleware = [Middleware]() + private var routeMiddleware: MockRoutesMiddleware? + var notFoundHandler: HandlerClosure? init(responseFactory: ResponseFactory) { self.responseFactory = responseFactory @@ -43,18 +28,24 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { try start(port) { (channel) -> EventLoopFuture in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { self.httpHandler = MockNIOHTTPHandler(responseFactory: self.responseFactory, + router: self.router, + middleware: self.middleware, notFoundHandler: self.notFoundHandler) return channel.pipeline.addHandler(self.httpHandler!) } } } + func register(route: MockHTTPRoute, handler: HandlerClosure?) { + self.router.register(route: route, handler: handler) + } + func add(middleware: Middleware) { - httpHandler?.middleware.append(middleware) + self.middleware.append(middleware) } func has(middlewareOfType type: T.Type) -> Bool where T: Middleware { - return (httpHandler?.middleware ?? []).contains { $0 is T } + return (self.middleware ?? []).contains { $0 is T } } } From 757e08668a08889e51b38a1a340a72623abe2dcf Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Thu, 10 Jun 2021 15:07:31 +0100 Subject: [PATCH 04/15] Hash conflicts. --- Shock/Classes/MockHTTPRoute.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Shock/Classes/MockHTTPRoute.swift b/Shock/Classes/MockHTTPRoute.swift index 792eb85..ffea481 100644 --- a/Shock/Classes/MockHTTPRoute.swift +++ b/Shock/Classes/MockHTTPRoute.swift @@ -224,20 +224,26 @@ extension MockHTTPRoute: Hashable { public func hash(into hasher: inout Hasher) { switch self { case .simple(method: let method, urlPath: let urlPath, _, _): + hasher.combine("simple") hasher.combine(method) hasher.combine(urlPath) case .custom(method: let method, urlPath: let urlPath, query: let query, _, _, _, _): + hasher.combine("custom") hasher.combine(method) hasher.combine(urlPath) hasher.combine(query) case .template(method: let method, urlPath: let urlPath, _, _, _): + hasher.combine("template") hasher.combine(method) hasher.combine(urlPath) case .redirect(urlPath: let urlPath, _): + hasher.combine("redirect") hasher.combine(urlPath) case .collection(routes: let routes): + hasher.combine("collection") hasher.combine(routes) case .timeout(method: let method, urlPath: let urlPath, _): + hasher.combine("timeout") hasher.combine(method) hasher.combine(urlPath) } From c0df7e4c978e156220e7be96a698bb4e505e9116 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Thu, 10 Jun 2021 16:50:09 +0100 Subject: [PATCH 05/15] Don't register routes without a URL path. --- Shock/Classes/NIO/MockNIOHTTPServer.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index be3ca46..464454a 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -37,6 +37,9 @@ class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { } func register(route: MockHTTPRoute, handler: HandlerClosure?) { + if let urlPath = route.urlPath, urlPath.isEmpty { + return + } self.router.register(route: route, handler: handler) } From 10eeefdc5952932e8f9f86423516cc5706535651 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Fri, 11 Jun 2021 09:52:28 +0100 Subject: [PATCH 06/15] Middleware ordering. --- Shock/Classes/NIO/MockNIOHTTPHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shock/Classes/NIO/MockNIOHTTPHandler.swift b/Shock/Classes/NIO/MockNIOHTTPHandler.swift index 0029118..b9edf1b 100644 --- a/Shock/Classes/NIO/MockNIOHTTPHandler.swift +++ b/Shock/Classes/NIO/MockNIOHTTPHandler.swift @@ -141,7 +141,7 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { if router.requiresRouteMiddleware { let routeMiddleware = MockRoutesMiddleware(router: self.router, responseFactory: self.responseFactory) - finalMiddleware.append(routeMiddleware) + finalMiddleware.insert(routeMiddleware, at: 0) } let responder = MiddlwareResponder(middleware: finalMiddleware, notFoundHandler: notFoundHandler) From a32f220a6d79afe576940e884698cd7dec9fcfbd Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Fri, 11 Jun 2021 13:47:29 +0100 Subject: [PATCH 07/15] Removed (unused) context property and made responseContext persist across middleware. --- Shock/Classes/Middleware/Middleware.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Shock/Classes/Middleware/Middleware.swift b/Shock/Classes/Middleware/Middleware.swift index e4bfc2e..fa0a643 100644 --- a/Shock/Classes/Middleware/Middleware.swift +++ b/Shock/Classes/Middleware/Middleware.swift @@ -73,7 +73,6 @@ class MiddlewareService { private let middleware: [Middleware] private let notFoundHandler: HandlerClosure? - private var context: MiddlewareContext? public init(middleware: [Middleware], notFoundHandler: HandlerClosure?) { self.middleware = middleware @@ -84,23 +83,24 @@ class MiddlewareService { let promise = request.eventLoop.makePromise(of: MiddlewareContext?.self) request.eventLoop.execute { - let context = self.executeAll(forRequest: request, middleware: self.middleware) + // _MiddlewareResponseContext is a reference type that is updated across the registered middlewares + let responseContext = _MiddlewareResponseContext() + let context = self.executeAll(forRequest: request, middleware: self.middleware, responseContext: responseContext) promise.succeed(context) } return promise.futureResult } - private func executeAll(forRequest request: MockNIOHTTPRequest, middleware: [Middleware]) -> MiddlewareContext? { + private func executeAll(forRequest request: MockNIOHTTPRequest, middleware: [Middleware], responseContext: MiddlewareResponseContext) -> MiddlewareContext? { let requestContext = _MiddlewareRequestContext(request: request) - let responseContext = _MiddlewareResponseContext() let context = _MiddlewareContext(requestContext: requestContext, responseContext: responseContext, notFoundHandler: notFoundHandler) { if (middleware.count - 1) > 0 { - self.executeAll(forRequest: request, middleware: Array(middleware[1...])) + self.executeAll(forRequest: request, middleware: Array(middleware[1...]), responseContext: responseContext) } } From 84603529a7abe4f8fe1c3b0b44b0545de98d8383 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Fri, 11 Jun 2021 15:32:22 +0100 Subject: [PATCH 08/15] Just trying things now. --- Shock/Classes/NIO/MockNIOHTTPHandler.swift | 27 ---------------------- Shock/Classes/NIO/MockNIOHTTPServer.swift | 27 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Shock/Classes/NIO/MockNIOHTTPHandler.swift b/Shock/Classes/NIO/MockNIOHTTPHandler.swift index b9edf1b..c58602d 100644 --- a/Shock/Classes/NIO/MockNIOHTTPHandler.swift +++ b/Shock/Classes/NIO/MockNIOHTTPHandler.swift @@ -180,30 +180,3 @@ struct MiddlwareResponder { return middlewareService.executeAll(forRequest: request) } } - -struct MockNIOHTTPRouter: MockHttpRouter { - typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] - private var routes = [String: RouteHandlerMapping]() - - var requiresRouteMiddleware: Bool { - !routes.isEmpty - } - - func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { - guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } - let methodRoutes = routes[method] ?? RouteHandlerMapping() - for (candidate, handler) in methodRoutes { - if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { - return handler - } - } - return nil - } - - mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) { - guard let method = route.method?.rawValue else { return } - var methodRoutes = routes[method] ?? RouteHandlerMapping() - methodRoutes[route] = handler - routes[method] = methodRoutes - } -} diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index 464454a..6f3d537 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -62,3 +62,30 @@ struct MockNIOHTTPRequest: MockHttpRequest { var address: String? var params: [String : String] } + +struct MockNIOHTTPRouter: MockHttpRouter { + typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] + private var routes = [MockHTTPMethod: RouteHandlerMapping]() + + var requiresRouteMiddleware: Bool { + !routes.isEmpty + } + + func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { + guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } + let methodRoutes = routes[httpMethod] ?? RouteHandlerMapping() + for (candidate, handler) in methodRoutes { + if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { + return handler + } + } + return nil + } + + mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) { + guard let method = route.method else { return } + var methodRoutes = routes[method] ?? RouteHandlerMapping() + methodRoutes[route] = handler + routes[method] = methodRoutes + } +} From e02669b821b102346f7eef3c943413332b92f055 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Mon, 21 Jun 2021 11:32:14 +0100 Subject: [PATCH 09/15] More checks. --- Shock/Classes/MockHTTPRoute.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Shock/Classes/MockHTTPRoute.swift b/Shock/Classes/MockHTTPRoute.swift index ffea481..e88356e 100644 --- a/Shock/Classes/MockHTTPRoute.swift +++ b/Shock/Classes/MockHTTPRoute.swift @@ -205,6 +205,8 @@ extension MockHTTPRoute: Hashable { } public func matches(method: MockHTTPMethod, path: String, params: [String:String], headers: [String:String]) -> Bool { + guard !method.rawValue.isEmpty else { return false } + guard !path.isEmpty else { return false } switch self { case .simple: return MockHTTPRoute.simple(method: method, urlPath: path, code: 0, filename: nil) == self From f9d1a9b888f22d40c25f532e84d0d84d2027bc7f Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Mon, 21 Jun 2021 13:17:31 +0100 Subject: [PATCH 10/15] Added tests. --- Example/Shock.xcodeproj/project.pbxproj | 4 ++ .../Fixtures/testSimpleRouteWithVariables.txt | 1 + Example/Tests/RouteTests.swift | 47 ++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Example/Tests/Fixtures/testSimpleRouteWithVariables.txt diff --git a/Example/Shock.xcodeproj/project.pbxproj b/Example/Shock.xcodeproj/project.pbxproj index 3dfa43f..ed024b1 100644 --- a/Example/Shock.xcodeproj/project.pbxproj +++ b/Example/Shock.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0463BE14252DA52700C18221 /* ClosureMiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0463BE13252DA52700C18221 /* ClosureMiddlewareTests.swift */; }; 0463BE1A252DA54700C18221 /* ShockTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0463BE19252DA54700C18221 /* ShockTestCase.swift */; }; + 142CB1172680B93100E0B3F4 /* testSimpleRouteWithVariables.txt in Resources */ = {isa = PBXBuildFile; fileRef = 142CB1162680B93100E0B3F4 /* testSimpleRouteWithVariables.txt */; }; 14739D0D25262B640031286B /* SocketServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14739D0C25262B640031286B /* SocketServerTests.swift */; }; 14C308E82649781700B85451 /* testCustomRoute2.txt in Resources */ = {isa = PBXBuildFile; fileRef = 14C308E72649781700B85451 /* testCustomRoute2.txt */; }; 28CF36BB9C72A116B41A9CCA /* Pods_Shock_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 504237DD0DF4E81BD4C74B63 /* Pods_Shock_Example.framework */; }; @@ -48,6 +49,7 @@ /* Begin PBXFileReference section */ 0463BE13252DA52700C18221 /* ClosureMiddlewareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureMiddlewareTests.swift; sourceTree = ""; }; 0463BE19252DA54700C18221 /* ShockTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShockTestCase.swift; sourceTree = ""; }; + 142CB1162680B93100E0B3F4 /* testSimpleRouteWithVariables.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = testSimpleRouteWithVariables.txt; sourceTree = ""; }; 14739D0C25262B640031286B /* SocketServerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketServerTests.swift; sourceTree = ""; }; 14C308E72649781700B85451 /* testCustomRoute2.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = testCustomRoute2.txt; sourceTree = ""; }; 2AF495106DF32828591B5E10 /* Pods-Shock_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Shock_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Shock_Tests/Pods-Shock_Tests.debug.xcconfig"; sourceTree = ""; }; @@ -211,6 +213,7 @@ children = ( 6DF676FB1F8793F700E1783A /* testTemplatedRoute.mustache */, 6D718E361F8777660070B764 /* testSimpleRoute.txt */, + 142CB1162680B93100E0B3F4 /* testSimpleRouteWithVariables.txt */, 6D718E381F877AB30070B764 /* testRedirectRoute.txt */, 6D7BDCF51FA72BC300488DB5 /* testCustomRoute.txt */, 14C308E72649781700B85451 /* testCustomRoute2.txt */, @@ -336,6 +339,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 142CB1172680B93100E0B3F4 /* testSimpleRouteWithVariables.txt in Resources */, 14C308E82649781700B85451 /* testCustomRoute2.txt in Resources */, 6DF676FC1F8793F700E1783A /* testTemplatedRoute.mustache in Resources */, 6D718E391F877AB30070B764 /* testRedirectRoute.txt in Resources */, diff --git a/Example/Tests/Fixtures/testSimpleRouteWithVariables.txt b/Example/Tests/Fixtures/testSimpleRouteWithVariables.txt new file mode 100644 index 0000000..d72dcd9 --- /dev/null +++ b/Example/Tests/Fixtures/testSimpleRouteWithVariables.txt @@ -0,0 +1 @@ +testSimpleRoute (with variables) test fixture diff --git a/Example/Tests/RouteTests.swift b/Example/Tests/RouteTests.swift index 78eb14d..ff782c8 100644 --- a/Example/Tests/RouteTests.swift +++ b/Example/Tests/RouteTests.swift @@ -26,12 +26,57 @@ class RouteTests: ShockTestCase { } func testSimpleRouteWithVariables() { - let route: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/:foo", code: 200, filename: "testSimpleRoute.txt") + let route: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/:foo", code: 200, filename: "testSimpleRouteWithVariables.txt") server.setup(route: route) let expectation = self.expectation(description: "Expect 200 response with response body") HTTPClient.get(url: "\(server.hostURL)/simple/1") { code, body, headers, error in + XCTAssertEqual(code, 200) + XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") + expectation.fulfill() + } + self.waitForExpectations(timeout: 2.0, handler: nil) + } + + func testSimpleRouteWithAndWithoutVariables() { + let withoutRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/withoutvariables", code: 200, filename: "testSimpleRoute.txt") + let withRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/withvariables/:foo", code: 200, filename: "testSimpleRouteWithVariables.txt") + server.setup(route: .collection(routes: [withRoute, withoutRoute])) + + let expectation = self.expectation(description: "Expect 200 response with response body") + + HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, headers, error in + XCTAssertEqual(code, 200) + XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") + expectation.fulfill() + } + self.waitForExpectations(timeout: 2.0, handler: nil) + } + + func testSimpleRouteWithEmptyURLPath() { + let withoutRoute: MockHTTPRoute = .simple(method: .get, urlPath: "", code: 200, filename: "testSimpleRoute.txt") + let withRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/withvariables/:foo", code: 200, filename: "testSimpleRouteWithVariables.txt") + server.setup(route: .collection(routes: [withRoute, withoutRoute])) + + let expectation = self.expectation(description: "Expect 200 response with response body") + + HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, headers, error in + XCTAssertEqual(code, 200) + XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") + expectation.fulfill() + } + self.waitForExpectations(timeout: 2.0, handler: nil) + } + + func testSimpleRouteWithEmptyURLPathAlternate() { + let withoutRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/simple/withoutvariables", code: 200, filename: "testSimpleRoute.txt") + let withRoute: MockHTTPRoute = .simple(method: .get, urlPath: "", code: 200, filename: "testSimpleRouteWithVariables.txt") + server.setup(route: .collection(routes: [withRoute, withoutRoute])) + + let expectation = self.expectation(description: "Expect 200 response with response body") + + HTTPClient.get(url: "\(server.hostURL)/simple/withoutvariables") { code, body, headers, error in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() From 8f287f40442a37cfdd01a7f1df4b9292f5bd3b92 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Mon, 21 Jun 2021 13:28:03 +0100 Subject: [PATCH 11/15] More case sensitivity removal. --- Shock/Classes/NIO/MockNIOHTTPServer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index 6f3d537..460fc65 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -72,7 +72,7 @@ struct MockNIOHTTPRouter: MockHttpRouter { } func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { - guard let httpMethod = MockHTTPMethod(rawValue: method) else { return nil } + guard let httpMethod = MockHTTPMethod(rawValue: method.uppercased()) else { return nil } let methodRoutes = routes[httpMethod] ?? RouteHandlerMapping() for (candidate, handler) in methodRoutes { if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { From 2543eb0353aafbebcf59aed4aa2773c8d1287b58 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Mon, 21 Jun 2021 16:59:09 +0100 Subject: [PATCH 12/15] Synthesised Hashable conformance. --- Shock/Classes/MockHTTPRoute.swift | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/Shock/Classes/MockHTTPRoute.swift b/Shock/Classes/MockHTTPRoute.swift index e88356e..602b6f8 100644 --- a/Shock/Classes/MockHTTPRoute.swift +++ b/Shock/Classes/MockHTTPRoute.swift @@ -32,7 +32,7 @@ public enum MockHTTPRoute { urlPath: String, code: Int, filename: String?, - templateInfo: [String: Any?] + templateInfo: [String: AnyHashable?] ) case redirect( @@ -222,34 +222,6 @@ extension MockHTTPRoute: Hashable { return MockHTTPRoute.timeout(method: method, urlPath: path, timeoutInSeconds: 0) == self } } - - public func hash(into hasher: inout Hasher) { - switch self { - case .simple(method: let method, urlPath: let urlPath, _, _): - hasher.combine("simple") - hasher.combine(method) - hasher.combine(urlPath) - case .custom(method: let method, urlPath: let urlPath, query: let query, _, _, _, _): - hasher.combine("custom") - hasher.combine(method) - hasher.combine(urlPath) - hasher.combine(query) - case .template(method: let method, urlPath: let urlPath, _, _, _): - hasher.combine("template") - hasher.combine(method) - hasher.combine(urlPath) - case .redirect(urlPath: let urlPath, _): - hasher.combine("redirect") - hasher.combine(urlPath) - case .collection(routes: let routes): - hasher.combine("collection") - hasher.combine(routes) - case .timeout(method: let method, urlPath: let urlPath, _): - hasher.combine("timeout") - hasher.combine(method) - hasher.combine(urlPath) - } - } } extension String { From b599b6fb5b50fe411dc40f6dc4bb9f75cb240639 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Wed, 23 Jun 2021 14:26:14 +0100 Subject: [PATCH 13/15] Gave up on hashable routes. --- Example/Tests/RouteTests.swift | 99 ----------------------- Shock/Classes/MockHTTPRoute.swift | 2 +- Shock/Classes/NIO/MockNIOHTTPServer.swift | 25 ++++-- 3 files changed, 18 insertions(+), 108 deletions(-) diff --git a/Example/Tests/RouteTests.swift b/Example/Tests/RouteTests.swift index ff782c8..e77f302 100644 --- a/Example/Tests/RouteTests.swift +++ b/Example/Tests/RouteTests.swift @@ -236,103 +236,4 @@ class RouteTests: ShockTestCase { ]) XCTAssertNotEqual(route1, route2, "Paths are different, should not be equal") } - - func testSimpleRouteHash() { - let route1 = MockHTTPRoute.simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) - let route2 = MockHTTPRoute.simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.simple(method: .post, urlPath: "foo/bar", code: 200, filename: nil) - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } - - func testCustomRouteHash() { - let route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - let route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.custom(method: .post, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } - - func testTemplateRouteHash() { - let route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - let route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.template(method: .post, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } - - func testRedirectRouteHash() { - let route1 = MockHTTPRoute.redirect(urlPath: "foo/bar", destination: "bar/foo") - let route2 = MockHTTPRoute.redirect(urlPath: "foo/bar", destination: "bar/foo") - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.redirect(urlPath: "bar/foo", destination: "bar/foo") - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } - - func testCollectionRouteHash() { - let route1 = MockHTTPRoute.collection(routes: [ - .simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) - ]) - let route2 = MockHTTPRoute.collection(routes: [ - .simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) - ]) - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.collection(routes: [ - .simple(method: .post, urlPath: "foo/bar", code: 200, filename: nil) - ]) - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } - - func testTimeoutRouteHash() { - let route1 = MockHTTPRoute.timeout(method: .get, urlPath: "foo/bar", timeoutInSeconds: 1) - let route2 = MockHTTPRoute.timeout(method: .get, urlPath: "foo/bar", timeoutInSeconds: 1) - var dict = [MockHTTPRoute : String]() - dict[route1] = "Foo" - XCTAssertEqual(dict.count, 1, "There should be one element in the dictionary") - dict[route2] = "Bar" - XCTAssertEqual(dict.count, 1, "There should still be one element in the dictionary") - let route3 = MockHTTPRoute.timeout(method: .post, urlPath: "foo/bar", timeoutInSeconds: 1) - dict[route3] = "Foo" - XCTAssertEqual(dict.count, 2, "There should now be two elements in the dictionary") - XCTAssertEqual(dict[route2], "Bar") - XCTAssertEqual(dict[route3], "Foo") - } } diff --git a/Shock/Classes/MockHTTPRoute.swift b/Shock/Classes/MockHTTPRoute.swift index 602b6f8..8bc2b31 100644 --- a/Shock/Classes/MockHTTPRoute.swift +++ b/Shock/Classes/MockHTTPRoute.swift @@ -154,7 +154,7 @@ public enum MockHTTPRoute { /// The philosophy for Equatable/Hashable `MockHTTPRoute` is anything in the request /// part of the route (e.g. `method` or `urlPath`) are part of the identify of the route -extension MockHTTPRoute: Hashable { +extension MockHTTPRoute: Equatable { public static func == (lhs: MockHTTPRoute, rhs: MockHTTPRoute) -> Bool { if case MockHTTPRoute.simple(let lhsMethod, let lhsUrlPath, let _, _) = lhs, case MockHTTPRoute.simple(let rhsMethod, let rhsUrlPath, let _, _) = rhs { diff --git a/Shock/Classes/NIO/MockNIOHTTPServer.swift b/Shock/Classes/NIO/MockNIOHTTPServer.swift index 460fc65..65fabdd 100644 --- a/Shock/Classes/NIO/MockNIOHTTPServer.swift +++ b/Shock/Classes/NIO/MockNIOHTTPServer.swift @@ -63,9 +63,13 @@ struct MockNIOHTTPRequest: MockHttpRequest { var params: [String : String] } +struct RouteHandlerMapping { + let route: MockHTTPRoute + let handler: HandlerClosure +} + struct MockNIOHTTPRouter: MockHttpRouter { - typealias RouteHandlerMapping = [MockHTTPRoute: HandlerClosure] - private var routes = [MockHTTPMethod: RouteHandlerMapping]() + private var routes = [MockHTTPMethod: [RouteHandlerMapping]]() var requiresRouteMiddleware: Bool { !routes.isEmpty @@ -73,10 +77,10 @@ struct MockNIOHTTPRouter: MockHttpRouter { func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { guard let httpMethod = MockHTTPMethod(rawValue: method.uppercased()) else { return nil } - let methodRoutes = routes[httpMethod] ?? RouteHandlerMapping() - for (candidate, handler) in methodRoutes { - if candidate.matches(method: httpMethod, path: path, params: params, headers: headers) { - return handler + let methodRoutes = routes[httpMethod] ?? [RouteHandlerMapping]() + for mapping in methodRoutes { + if mapping.route.matches(method: httpMethod, path: path, params: params, headers: headers) { + return mapping.handler } } return nil @@ -84,8 +88,13 @@ struct MockNIOHTTPRouter: MockHttpRouter { mutating func register(route: MockHTTPRoute, handler: HandlerClosure?) { guard let method = route.method else { return } - var methodRoutes = routes[method] ?? RouteHandlerMapping() - methodRoutes[route] = handler + var methodRoutes = routes[method] ?? [RouteHandlerMapping]() + if methodRoutes.contains() { $0.route == route } { + methodRoutes = methodRoutes.filter({ $0.route != route }) + } + if let handler = handler { + methodRoutes.append(RouteHandlerMapping(route: route, handler: handler)) + } routes[method] = methodRoutes } } From 52f7f467023e889cebea46e9f89aed4dd2fac84e Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Thu, 24 Jun 2021 10:46:12 +0100 Subject: [PATCH 14/15] Equatable wasn't dealing with the case where both url paths were templates. Added tests. --- Example/Tests/RouteTests.swift | 12 ++++++++++++ Shock/Classes/MockHTTPRoute.swift | 13 ++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Example/Tests/RouteTests.swift b/Example/Tests/RouteTests.swift index e77f302..1632d5c 100644 --- a/Example/Tests/RouteTests.swift +++ b/Example/Tests/RouteTests.swift @@ -121,6 +121,18 @@ class RouteTests: ShockTestCase { } func testSimpleRouteEquivalence() { + var route1 = MockHTTPRoute.simple(method: .get, urlPath: "/restaurants/bypostcode/:postcode", code: 200, filename: nil) + var route2 = MockHTTPRoute.simple(method: .get, urlPath: "/restaurants/bypostcode/:postcode", code: 200, filename: nil) + XCTAssertEqual(route1, route2, "Simple gets should be equal") + route1 = MockHTTPRoute.simple(method: .get, urlPath: "/restaurants/bypostcode/:postcode", code: 200, filename: nil) + route2 = MockHTTPRoute.simple(method: .get, urlPath: "/restaurants/bypostcode/AR511AA", code: 200, filename: nil) + XCTAssertEqual(route1, route2, "Simple gets should be equal") + route1 = MockHTTPRoute.simple(method: .get, urlPath: "/restaurants/bypostcode/:postcode", code: 200, filename: nil) + route2 = MockHTTPRoute.simple(method: .get, urlPath: "restaurants/:id/reviews", code: 200, filename: nil) + XCTAssertNotEqual(route1, route2, "Paths are different, should not be equal") + } + + func testSimpleRouteWithVariablesEquivalence() { var route1 = MockHTTPRoute.simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) var route2 = MockHTTPRoute.simple(method: .get, urlPath: "foo/bar", code: 200, filename: nil) XCTAssertEqual(route1, route2, "Simple gets should be equal") diff --git a/Shock/Classes/MockHTTPRoute.swift b/Shock/Classes/MockHTTPRoute.swift index 8bc2b31..141c3da 100644 --- a/Shock/Classes/MockHTTPRoute.swift +++ b/Shock/Classes/MockHTTPRoute.swift @@ -226,17 +226,20 @@ extension MockHTTPRoute: Equatable { extension String { func pathMatchesStrippingVariables(_ other: String) -> Bool { + let bothTemplates = self.contains() { $0 == ":" } && other.contains() { $0 == ":" } let parts = self.split(separator: "/") let otherParts = other.split(separator: "/") guard parts.count == otherParts.count else { return false } var match = true for (index, part) in parts.enumerated() { - if part.hasPrefix(":") { - continue - } let otherPart = otherParts[index] - if otherPart.hasPrefix(":") { - continue + if !bothTemplates { + if part.hasPrefix(":") { + continue + } + if otherPart.hasPrefix(":") { + continue + } } match = part.lowercased() == otherPart.lowercased() if !match { From ef0376326639f662140c1cb3874366fe2b7b24f0 Mon Sep 17 00:00:00 2001 From: Antonio Strijdom Date: Fri, 25 Jun 2021 15:02:43 +0100 Subject: [PATCH 15/15] Bumped pod version --- Example/Podfile.lock | 4 ++-- Shock.podspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/Podfile.lock b/Example/Podfile.lock index d805c01..95f448a 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -7,7 +7,7 @@ PODS: - GRMustache.swift (4.0.1) - JustLog (3.1.3): - SwiftyBeaver (~> 1.9.1) - - Shock (6.0.1): + - Shock (6.0.2): - GRMustache.swift (~> 4.0.1) - SwiftNIOHTTP1 (~> 2.22.1) - SwiftNIO (2.22.1): @@ -54,7 +54,7 @@ SPEC CHECKSUMS: CNIOSHA1: fd4d38fb42778ed971a75d87530f842106d4b4f4 GRMustache.swift: a6436504284b22b4b05daf5f46f77bd3fe00a9a2 JustLog: c1b12fe8fc6a7c22cc31dd874fe7776eaa7089d2 - Shock: 31b6786d7716dba5c6dff68c9e3949ab96aebf30 + Shock: 708aafb186e31cef487afbf87ce7e8cac337fb8e SwiftNIO: 763188229de166e046cc8975383cca388832e992 SwiftNIOConcurrencyHelpers: 34d2e9d4d2b7886fa765086e845e6b19417be927 SwiftNIOHTTP1: c0a9ce48ba256f349af4f09648a0e0939b11a65c diff --git a/Shock.podspec b/Shock.podspec index 60c95f8..8cb9954 100644 --- a/Shock.podspec +++ b/Shock.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'Shock' - s.version = '6.0.1' + s.version = '6.0.2' s.summary = 'A HTTP mocking framework written in Swift.' s.description = <<-DESC