Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type-Safe Request Routing in Swift

Type-Safe Request Routing in Swift

Since the announcement of the open sourcing of Swift at WWDC and the promise of Linux support, developers have been working on building web frameworks for Swift. In this talk, Grant Butler takes on one portion of a web framework, request routing, by leveraging Swift’s powerful type system. He’ll also touch upon some of the work arounds and tricks he discovered while building this web framework component.

Video presentation: https://vimeo.com/172310206

Grant J. Butler

May 25, 2016
Tweet

Other Decks in Programming

Transcript

  1. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  2. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  3. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  4. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  5. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  6. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  7. PERFECT GET /POST/{ID} Routing.Routes["GET", "/post/{id}"] = showPost func showPost(request: WebRequest,

    _ response: WebResponse) { guard let postID = request.urlVariables["id"] else { response.appendBody(string: "<html><body><h1>Invalid Post ID</h1></body></ html>") response.requestCompleted() return } let postHTML = // Fetch and render post response.appendBody(string: "<html><body>\(postHTML)</body></html>") response.requestCompleted() }
  8. SWITCHBOARD BLOG ROUTES let router = Router() router.get("/post/:postID") { (request,

    postID: Int) -> Response in let postHTML = // Fetch and render post return Response.html("<html><body>\(postHTML)</body></html>") } router.get("/post/:postID/comment/:commentID") { (request, postID: Int, commentID: Int) -> Response in let commentHTML = // Fetch and render comment return Response.html("<html><body>\(commentHTML)</body></html>") }
  9. SWITCHBOARD BLOG ROUTES let router = Router() router.get("/post/:postID") { (request,

    postID: Int) -> Response in let postHTML = // Fetch and render post return Response.html("<html><body>\(postHTML)</body></html>") } router.get("/post/:postID/comment/:commentID") { (request, postID: Int, commentID: Int) -> Response in let commentHTML = // Fetch and render comment return Response.html("<html><body>\(commentHTML)</body></html>") }
  10. SWITCHBOARD BLOG ROUTES let router = Router() router.get("/post/:postID") { (request,

    postID: Int) -> Response in let postHTML = // Fetch and render post return Response.html("<html><body>\(postHTML)</body></html>") } router.get("/post/:postID/comment/:commentID") { (request, postID: Int, commentID: Int) -> Response in let commentHTML = // Fetch and render comment return Response.html("<html><body>\(commentHTML)</body></html>") }
  11. SWITCHBOARD WILDCARDPATH.SWIFT protocol PathType { static func fromString(string: String) ->

    Self? } struct WildcardPath { let path: Path let pathTypes: [PathType.Type] func matches(path: Path) -> Bool { // ...
 } } let path = WildcardPath(path: "/post/:postID", pathTypes: [Int.self]) path.matches("/post/123") // true path.matches("/post/my-awesome-post") // false
  12. SWITCHBOARD WILDCARDPATH.SWIFT protocol PathType { static func fromString(string: String) ->

    Self? } struct WildcardPath { let path: Path let pathTypes: [PathType.Type] func matches(path: Path) -> Bool { // ...
 } } let path = WildcardPath(path: "/post/:postID", pathTypes: [Int.self]) path.matches("/post/123") // true path.matches("/post/my-awesome-post") // false
  13. SWITCHBOARD WILDCARDPATH.SWIFT protocol PathType { static func fromString(string: String) ->

    Self? } struct WildcardPath { let path: Path let pathTypes: [PathType.Type] func matches(path: Path) -> Bool { // ...
 } } let path = WildcardPath(path: "/post/:postID", pathTypes: [Int.self]) path.matches("/post/123") // true path.matches("/post/my-awesome-post") // false
  14. SWITCHBOARD WILDCARDPATH.SWIFT protocol PathType { static func fromString(string: String) ->

    Self? } struct WildcardPath { let path: Path let pathTypes: [PathType.Type] func matches(path: Path) -> Bool { // ...
 } } let path = WildcardPath(path: "/post/:postID", pathTypes: [Int.self]) path.matches("/post/123") // true path.matches("/post/my-awesome-post") // false
  15. SWITCHBOARD WILDCARDPATH.SWIFT protocol PathType { static func fromString(string: String) ->

    Self? } struct WildcardPath { let path: Path let pathTypes: [PathType.Type] func matches(path: Path) -> Bool { // ...
 } } let path = WildcardPath(path: "/post/:postID", pathTypes: [Int.self]) path.matches("/post/123") // true path.matches("/post/my-awesome-post") // false
  16. SWITCHBOARD ROUTE.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct Route { let method: Method let wildcardPath: WildcardPath let handler: RouteHandlingType func matches(request: Request) -> Bool { guard request.method == method else { return false } guard wildcardPath.matches(request.path) else { return false } return true } }
  17. SWITCHBOARD ROUTE.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct Route { let method: Method let wildcardPath: WildcardPath let handler: RouteHandlingType func matches(request: Request) -> Bool { guard request.method == method else { return false } guard wildcardPath.matches(request.path) else { return false } return true } }
  18. SWITCHBOARD ROUTE.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct Route { let method: Method let wildcardPath: WildcardPath let handler: RouteHandlingType func matches(request: Request) -> Bool { guard request.method == method else { return false } guard wildcardPath.matches(request.path) else { return false } return true } }
  19. SWITCHBOARD ROUTE.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct Route { let method: Method let wildcardPath: WildcardPath let handler: RouteHandlingType func matches(request: Request) -> Bool { guard request.method == method else { return false } guard wildcardPath.matches(request.path) else { return false } return true } }
  20. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  21. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  22. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  23. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  24. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  25. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func get(path: Path, handler: (Request) -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: []) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg<A>(handler: handler) let wildcardPath = WildcardPath(path: path, pathTypes: [A.self]) routes += [Route(method: .GET, wildcardPath: wildcardPath, handler: routeHandler)] } }
  26. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func route(request: Request) throws -> Response { for route in routes where route.matches(request) { return route.handle(request) } throw RoutingError.NotFound(request.method, request.path) } }
  27. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func route(request: Request) throws -> Response { for route in routes where route.matches(request) { return route.handle(request) } throw RoutingError.NotFound(request.method, request.path) } }
  28. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func route(request: Request) throws -> Response { for route in routes where route.matches(request) { return route.handle(request) } throw RoutingError.NotFound(request.method, request.path) } }
  29. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func route(request: Request) throws -> Response { for route in routes where route.matches(request) { return route.handle(request) } throw RoutingError.NotFound(request.method, request.path) } }
  30. SWITCHBOARD ROUTER.SWIFT final class Router { var routes: [Route] =

    [] func route(request: Request) throws -> Response { for route in routes where route.matches(request) { return route.handle(request) } throw RoutingError.NotFound(request.method, request.path) } }
  31. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  32. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  33. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  34. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  35. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  36. SWITCHBOARD ROUTE.SWIFT struct Route { let handler: RouteHandlingType public func

    handle(request: Request) throws -> Response { let matchedRequest = MatchedRequest(request: request, route: self) guard let response = handler.perform(matchedRequest) else { throw RoutingError.NotFound(request.method, request.path) } return response } }
  37. SWITCHBOARD ROUTEHANDLERS.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct RouteHandlerWithOneArg<A: PathType>: RouteHandlingType { let handler: (Request, A) -> Response func perform(request: MatchedRequest) -> Response? { guard let param1: A = request.paramAtIndex(0) else { return nil } return handler(request.request, param1) } }
  38. SWITCHBOARD ROUTEHANDLERS.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct RouteHandlerWithOneArg<A: PathType>: RouteHandlingType { let handler: (Request, A) -> Response func perform(request: MatchedRequest) -> Response? { guard let param1: A = request.paramAtIndex(0) else { return nil } return handler(request.request, param1) } }
  39. SWITCHBOARD ROUTEHANDLERS.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct RouteHandlerWithOneArg<A: PathType>: RouteHandlingType { let handler: (Request, A) -> Response func perform(request: MatchedRequest) -> Response? { guard let param1: A = request.paramAtIndex(0) else { return nil } return handler(request.request, param1) } }
  40. SWITCHBOARD ROUTEHANDLERS.SWIFT protocol RouteHandlingType { func perform(request: MatchedRequest) -> Response?

    } struct RouteHandlerWithOneArg<A: PathType>: RouteHandlingType { let handler: (Request, A) -> Response func perform(request: MatchedRequest) -> Response? { guard let param1: A = request.paramAtIndex(0) else { return nil } return handler(request.request, param1) } }
  41. SWITCHBOARD PROTOCOLS CONFORMING TO PROTOCOLS IN EXTENSIONS extension ErrorType: ResponseEncodable

    { func asResponse() -> Response { return Response.plainText("\(self)", statusCode: .InternalServerError) } }
  42. SWITCHBOARD PROTOCOLS CONFORMING TO PROTOCOLS IN EXTENSIONS extension ErrorType: ResponseEncodable

    { func asResponse() -> Response { return Response.plainText("\(self)", statusCode: .InternalServerError) } } Extension of protocol 'ErrorType' cannot have an inheritance clause
  43. SWITCHBOARD PROTOCOLS CONFORMING TO PROTOCOLS IN EXTENSIONS extension ErrorType: ResponseEncodable

    { func asResponse() -> Response { return Response.plainText("\(self)", statusCode: .InternalServerError) } } Extension of protocol 'ErrorType' cannot have an inheritance clause extension Response { init(error: ErrorType) { self = self.dynamicType.plainText("\(error)", statusCode: .InternalServerError) } }
  44. SWITCHBOARD GOTCHAS ▸ Protocols conforming to protocols in extensions ▸

    Class conformance to protocol with Self requirements
  45. SWITCHBOARD CLASS CONFORMANCE TO PROTOCOL WITH SELF REQUIREMENTS protocol PathType

    { static func fromString(string: String) -> Self? } extension NSUUID: PathType { static func fromString(string: String) -> Self? { return NSUUID(UUIDString: string) } }
  46. SWITCHBOARD CLASS CONFORMANCE TO PROTOCOL WITH SELF REQUIREMENTS protocol PathType

    { static func fromString(string: String) -> Self? } extension NSUUID: PathType { static func fromString(string: String) -> Self? { return NSUUID(UUIDString: string) } } Cannot convert return expression of type 'NSUUID?' to return type 'Self?'
  47. SWITCHBOARD CLASS CONFORMANCE TO PROTOCOL WITH SELF REQUIREMENTS protocol PathType

    { static func fromString(string: String) -> Self? } extension NSUUID: PathType { static func fromString(string: String) -> NSUUID? { return NSUUID(UUIDString: string) } }
  48. SWITCHBOARD CLASS CONFORMANCE TO PROTOCOL WITH SELF REQUIREMENTS protocol PathType

    { static func fromString(string: String) -> Self? } extension NSUUID: PathType { static func fromString(string: String) -> NSUUID? { return NSUUID(UUIDString: string) } } Method 'fromString' in non-final class 'NSUUID' must return `Self` to conform to protocol 'PathType'
  49. SWITCHBOARD CLASS CONFORMANCE TO PROTOCOL WITH SELF REQUIREMENTS protocol PathType

    { static func fromString(string: String) -> Self? } extension NSUUID: PathType { static func fromString(string: String) -> Self? { return self.init(UUIDString: string) } }
  50. SWITCHBOARD GOTCHAS ▸ Protocols conforming to protocols in extensions ▸

    Class conformance to protocol with Self requirements ▸ Generic structs with the same name
  51. SWITCHBOARD GENERIC STRUCTS WITH THE SAME NAME protocol RouteHandlingType {

    func perform(request: Request) } struct RouteHandler: RouteHandlingType { func perform(request: Request) { } } struct RouteHandler<A: PathType>: RouteHandlingType { func perform(request: Request) { } }
  52. SWITCHBOARD GENERIC STRUCTS WITH THE SAME NAME protocol RouteHandlingType {

    func perform(request: Request) } struct RouteHandler: RouteHandlingType { func perform(request: Request) { } } struct RouteHandler<A: PathType>: RouteHandlingType { func perform(request: Request) { } } Invalid redeclaration of 'RouteHandler'
  53. SWITCHBOARD GENERIC STRUCTS WITH THE SAME NAME protocol RouteHandlingType {

    func perform(request: Request) } struct RouteHandlerWithZeroArgs: RouteHandlingType { func perform(request: Request) { } } struct RouteHandlerWithOneArg<A: PathType>: RouteHandlingType { func perform(request: Request) { } }
  54. SWITCHBOARD GOTCHAS ▸ Protocols conforming to protocols in extensions ▸

    Class conformance to protocol with Self requirements ▸ Generic structs with the same name ▸ Automatic code generation
  55. SWITCHBOARD ROUTEHANDLERS.SWIFT struct RouteHandlerWithZeroArgs { } struct RouteHandlerWithOneArg<A: PathType> {

    } struct RouteHandlerWithTwoArgs<A: PathType, B: PathType> { } struct RouteHandlerWithThreeArgs<A: PathType, B: PathType, C: PathType> { } struct RouteHandlerWithFourArgs<A: PathType, B: PathType, C: PathType, D: PathType> { } struct RouteHandlerWithFiveArgs<A: PathType, B: PathType, C: PathType, D: PathType, E: PathType> { } struct RouteHandlerWithSixArgs<A: PathType, B: PathType, C: PathType, D: PathType, E: PathType, F: PathType> { }
  56. SWITCHBOARD ROUTEREXTENSIONS.SWIFT extension Router { func get(path: Path, handler: (Request)

    -> Response) { let routeHandler = RouteHandlerWithZeroArgs(handler: handler) routes += [Route(method: .GET, path: path, pathTypes: [], handler: routeHandler)] } func get<A: PathType>(path: Path, handler: (Request, A) -> Response) { let routeHandler = RouteHandlerWithOneArg(handler: handler) routes += [Route(method: .GET, path: path, pathTypes: [A.self], handler: routeHandler)] } func get<A: PathType, B: PathType>(path: Path, handler: (Request, A, B) -> Response) { let routeHandler = RouteHandlerWithTwoArgs(handler: handler) routes += [Route(method: .GET, path: path, pathTypes: [A.self, B.self], handler: routeHandler)] } // ... }
  57. SWITCHBOARD GOTCHAS ▸ Protocols conforming to protocols in extensions ▸

    Class conformance to protocol with Self requirements ▸ Generic structs with the same name ▸ Automatic code generation