From bcbd30786295631cb94c6df5d73553ad5ceb0026 Mon Sep 17 00:00:00 2001 From: noppoman Date: Fri, 7 Oct 2016 19:44:34 +0900 Subject: [PATCH 1/7] init --- Package.swift | 18 +- Sources/Performance/main.swift | 46 +++++- Sources/Slimane/AsyncRouter.swift | 40 ----- Sources/Slimane/BasicAsyncMiddleware.swift | 21 --- Sources/Slimane/{ => HTTP}/Request.swift | 0 Sources/Slimane/HTTP/Response.swift | 73 +++++++++ Sources/Slimane/HTTP/Slimane+Server.swift | 121 ++++++++++++++ Sources/Slimane/Middleware/BodyParser.swift | 83 ++++++++++ .../Slimane/Middleware/Slimane+Static.swift | 69 ++++++++ Sources/Slimane/Response.swift | 30 ---- .../{AsyncRoute.swift => Route/Route.swift} | 45 +++-- Sources/Slimane/Router/Router.swift | 28 ++++ Sources/Slimane/Router/Slimane+Router.swift | 48 ++++++ Sources/Slimane/Slimane+Router.swift | 103 ------------ Sources/Slimane/Slimane+Server.swift | 94 ----------- Sources/Slimane/Slimane+Static.swift | 71 -------- Sources/Slimane/Slimane.swift | 35 +--- Sources/Slimane/Util/Regex.swift | 154 ++++++++++++++++++ Sources/WebAppExample/main.swift | 53 ++++++ 19 files changed, 702 insertions(+), 430 deletions(-) delete mode 100644 Sources/Slimane/AsyncRouter.swift delete mode 100644 Sources/Slimane/BasicAsyncMiddleware.swift rename Sources/Slimane/{ => HTTP}/Request.swift (100%) create mode 100644 Sources/Slimane/HTTP/Response.swift create mode 100644 Sources/Slimane/HTTP/Slimane+Server.swift create mode 100644 Sources/Slimane/Middleware/BodyParser.swift create mode 100644 Sources/Slimane/Middleware/Slimane+Static.swift delete mode 100644 Sources/Slimane/Response.swift rename Sources/Slimane/{AsyncRoute.swift => Route/Route.swift} (55%) create mode 100644 Sources/Slimane/Router/Router.swift create mode 100644 Sources/Slimane/Router/Slimane+Router.swift delete mode 100644 Sources/Slimane/Slimane+Router.swift delete mode 100644 Sources/Slimane/Slimane+Server.swift delete mode 100644 Sources/Slimane/Slimane+Static.swift create mode 100644 Sources/Slimane/Util/Regex.swift create mode 100644 Sources/WebAppExample/main.swift diff --git a/Package.swift b/Package.swift index 5385a77..98c8f16 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,12 @@ import PackageDescription let package = Package( - name: "Slimane", - targets: [ - Target(name: "Performance", dependencies: ["Slimane"]) + name: "Slimane", + targets: [ + Target(name: "Performance", dependencies: ["Slimane"]), + Target(name: "WebAppExample", dependencies: ["Slimane"]), ], - dependencies: [ - .Package(url: "https://github.com/noppoMan/Skelton.git", majorVersion: 0, minor: 8), - .Package(url: "https://github.com/slimane-swift/Time.git", majorVersion: 0, minor: 2), - .Package(url: "https://github.com/slimane-swift/POSIXRegex.git", majorVersion: 0, minor: 12), - .Package(url: "https://github.com/slimane-swift/AsyncResponderConvertible.git", majorVersion: 0, minor: 5), - .Package(url: "https://github.com/slimane-swift/AsyncHTTPSerializer.git", majorVersion: 0, minor: 2), - .Package(url: "https://github.com/slimane-swift/HTTPUpgradeAsync.git", majorVersion: 0, minor: 2), - ] + dependencies: [ + .Package(url: "https://github.com/noppoMan/Skelton.git", majorVersion: 0, minor: 10) + ] ) diff --git a/Sources/Performance/main.swift b/Sources/Performance/main.swift index fb3896f..5d75647 100644 --- a/Sources/Performance/main.swift +++ b/Sources/Performance/main.swift @@ -1,15 +1,45 @@ import Slimane -do { +if Cluster.isMaster { + for _ in 0.. AsyncRoute? -} - -public struct Router: AsyncRouter { - let respond: AsyncRespond - - public var routes: [AsyncRoute] = [] - - public init(_ respond: AsyncRespond) { - self.respond = respond - } - - public func respond(to request: Request, result: @escaping ((Void) throws -> Response) -> Void) { - return self.respond(to: request, result: result) - } - - public func match(_ request: Request) -> AsyncRoute? { - guard let path = request.path else { - return nil - } - - let request = request - for responder in routes { - if responder.regexp.matches(path) && request.method == responder.method { - return responder - } - } - return nil - } -} diff --git a/Sources/Slimane/BasicAsyncMiddleware.swift b/Sources/Slimane/BasicAsyncMiddleware.swift deleted file mode 100644 index b0cb39b..0000000 --- a/Sources/Slimane/BasicAsyncMiddleware.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// BasicAsyncMiddleware.swift -// Slimane -// -// Created by Yuki Takei on 4/18/16. -// -// - -public typealias AsyncMiddlewareHandler = (_ to: Request, _ next: AsyncResponder, _ result: @escaping ((Void) throws -> Response) -> Void) -> Void - -public struct BasicAsyncMiddleware: AsyncMiddleware { - let handler: AsyncMiddlewareHandler - - public init(_ handler: AsyncMiddlewareHandler){ - self.handler = handler - } - - public func respond(to request: Request, chainingTo next: AsyncResponder, result: @escaping ((Void) throws -> Response) -> Void) { - handler(request, next, result) - } -} diff --git a/Sources/Slimane/Request.swift b/Sources/Slimane/HTTP/Request.swift similarity index 100% rename from Sources/Slimane/Request.swift rename to Sources/Slimane/HTTP/Request.swift diff --git a/Sources/Slimane/HTTP/Response.swift b/Sources/Slimane/HTTP/Response.swift new file mode 100644 index 0000000..1fcd509 --- /dev/null +++ b/Sources/Slimane/HTTP/Response.swift @@ -0,0 +1,73 @@ +//// +//// Request.swift +//// Slimane +//// +//// Created by Yuki Takei on 4/16/16. +//// Copyright © 2016 MikeTOKYO. All rights reserved. +//// + +extension Response { + + public init(redirect location: String) { + let headers: Headers = ["Location": location, "Cache-Control": "max-age=0, no-cache, no-store"] + self.init(status: .movedPermanently, headers: headers, body: []) + } + + public init(status: Status = .ok, headers: Headers = [:], body: String) { + self.init( + status: status, + headers: headers, + body: .buffer(body.data) + ) + } + + var bodyLength: Int { + if case .buffer(let data) = body { + return data.count + } + return 0 + } + + public var isKeepAlive: Bool { + if version.minor == 0 { + return connection?.lowercased().index(of: "keep-alive") != nil + } + + return connection?.lowercased().index(of: "close") == nil + } + + public mutating func status(_ status: HTTPCore.Status) { + self.status = status + } + + public mutating func json(_ json: SwiftyJSON.JSON) { + self.headers["Content-Tyoe"] = "application/json" + do { + self.data(try json.rawData()) + } catch { + self.data("{}".data) + } + } + + public mutating func redirect(to location: String){ + self.status = .movedPermanently + self.headers["Location"] = location + self.headers["Cache-Control"] = "max-age=0, no-cache, no-store" + } + + public mutating func text(_ body: String) { + let data = body.data + self.body = .buffer(data) + self.headers["Content-Length"] = data.count.description + } + + public mutating func data(_ body: Data) { + self.body = .buffer(body) + self.headers["Content-Length"] = body.count.description + } + + public mutating func stream(_ body: @escaping (WritableStream, @escaping (Result) -> Void) -> Void) { + self.body = .writer(body) + self.headers["Transfer-Encoding"] = "chunked" + } +} diff --git a/Sources/Slimane/HTTP/Slimane+Server.swift b/Sources/Slimane/HTTP/Slimane+Server.swift new file mode 100644 index 0000000..0350178 --- /dev/null +++ b/Sources/Slimane/HTTP/Slimane+Server.swift @@ -0,0 +1,121 @@ +// +// Serve.swift +// Slimane +// +// Created by Yuki Takei on 4/16/16. +// +// + +extension Slimane { + + public func listen(host: String = "0.0.0.0", port: Int = 3000) throws { + let server = Skelton { [unowned self] result in + switch result { + case .onRequest(let request, let stream): + self.dispatch(request, stream) + case .onError(let error): + print(error) + default: + break //ignore eof + } + } + + // proxy settings + server.setNoDelay = self.setNodelay + server.keepAliveTimeout = self.keepAliveTimeout + server.backlog = self.backlog + + // bind & listen + if Cluster.isMaster { + try server.bind(host: host, port: port) + } + try server.listen() + } + + private func dispatch(_ request: Request, _ stream: DuplexStream){ + middlewares.chain(request: request, response: Response()) { [unowned self] chainer in + switch chainer { + case .respond(let response): + self.respond(request, response, stream) + + case .next(let request, let response): + if let (route, request) = self.router.matchedRoute(for: request) { + route.middlewares.chain(request: request, response: response) { [unowned self] chainer in + switch chainer { + case .respond(let response): + self.respond(request, response, stream) + + case .next(let request, let response): + route.respond(request, response) { chainer in + switch chainer { + case .respond(let response): + self.respond(request, response, stream) + + case .next(let request, let response): + self.respondError(MiddlewareError.noNextMiddleware, request, response, stream) + + case .error(let error): + self.respondError(error, request, response, stream) + } + } + + case .error(let error): + self.respondError(error, request, response, stream) + } + } + } else { + let error = RoutingError.routeNotFound(path: request.path ?? "/") + self.respondError(error, request, response, stream) + } + + case .error(let error): + self.respondError(error, request, Response(), stream) + } + } + } + + private func respondError(_ error: Error, _ request: Request, _ response: HTTPCore.Response, _ stream: DuplexStream){ + self.catchHandler(MiddlewareError.noNextMiddleware, request, response) { [unowned self] chainer in + var response = response + switch chainer { + case .respond(let response): + self.respond(request, response, stream) + + case .next(_): + response.status(.internalServerError) + response.text("\(MiddlewareError.noNextMiddleware)") + self.respond(request, response, stream) + + case .error(_): + response.status(.internalServerError) + response.text("Something went wrong.") + self.respond(request, response, stream) + } + } + } + + private func respond(_ request: HTTPCore.Request, _ response: HTTPCore.Response, _ stream: DuplexStream){ + var response = response + response.headers["Server"] = "Slimane" + + if response.contentType == nil { + response.contentType = mediaType(forFileExtension: "html")! + } + + ResponseSerializer(stream: stream).serialize(response) { [unowned self] result in + if case .failure(_) = result { + return stream.close() + } + + self.finallyHandler(request, response) + + if let upgradeConnection = response.upgradeConnection { + upgradeConnection(request, stream) + } + + if !request.isKeepAlive && response.upgradeConnection == nil { + stream.close() + } + } + } +} diff --git a/Sources/Slimane/Middleware/BodyParser.swift b/Sources/Slimane/Middleware/BodyParser.swift new file mode 100644 index 0000000..0d4d634 --- /dev/null +++ b/Sources/Slimane/Middleware/BodyParser.swift @@ -0,0 +1,83 @@ +// +// BodyParser.swift +// Slimane +// +// Created by Yuki Takei on 2016/10/07. +// +// + +@_exported import SwiftyJSON + +extension Request { + public var json: SwiftyJSON.JSON? { + get { + return self.storage["jsonBody"] as? SwiftyJSON.JSON + } + + set { + self.storage["jsonBody"] = newValue + } + } + + public var formData: URLEncodedForm? { + get { + return self.storage["formData"] as? URLEncodedForm + } + + set { + self.storage["formData"] = newValue + } + } +} + +public struct BodyParser { + + public struct URLEncoded: Middleware { + public init(){} + + public func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { + guard let contentType = request.contentType else { + return responder(.next(request, response)) + } + var request = request + + do { + if case .buffer(let data) = request.body { + switch (contentType.type, contentType.subtype) { + case ("application", "x-www-form-urlencoded"): + request.formData = try URLEncodedFormParser().parse(data: data) + default: + break + } + } + } catch { + responder(.error(error)) + return + } + + responder(.next(request, response)) + } + } + + public struct JSON: Middleware { + public init(){} + + public func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { + guard let contentType = request.contentType else { + return responder(.next(request, response)) + } + var request = request + + if case .buffer(let data) = request.body { + switch (contentType.type, contentType.subtype) { + case ("application", "json"): + request.json = SwiftyJSON.JSON(data: data) + default: + break + } + } + + responder(.next(request, response)) + } + } +} diff --git a/Sources/Slimane/Middleware/Slimane+Static.swift b/Sources/Slimane/Middleware/Slimane+Static.swift new file mode 100644 index 0000000..0c033c7 --- /dev/null +++ b/Sources/Slimane/Middleware/Slimane+Static.swift @@ -0,0 +1,69 @@ +// +// Static.swift +// Slimane +// +// Created by Yuki Takei on 4/19/16. +// +// + +//import CLibUv +// +public enum StaticMiddlewareError: Error { + case resourceNotFound(path: String) +} + +extension StaticMiddlewareError: CustomStringConvertible { + public var description: String { + switch(self) { + case .resourceNotFound(let resourceName): + return "\(resourceName) is not found" + } + } +} + +extension Slimane { + public struct Static: Middleware { + + let root: String + + let ignoreNotFoundInterruption: Bool + + public init(root: String, ignoreNotFoundInterruption: Bool = false){ + self.root = root + self.ignoreNotFoundInterruption = ignoreNotFoundInterruption + } + + public func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { + guard let path = request.path , let ext = path.split(separator: ".").last, let mediaType = mediaType(forFileExtension: ext) else { + return responder(.next(request, response)) + } + + var response = response + + File.read(path: root + path) { result in + switch result { + case .success(let data): + response.contentType = mediaType + response.data(data) + responder(.respond(response)) + case .failure(let error): + switch error { + case UVError.rawUvError(let code): + let e: Error + if code == -2 { + e = StaticMiddlewareError.resourceNotFound(path: path) + } else { + e = UVError.rawUvError(code: code) + } + responder(.error(e)) + default: + if self.ignoreNotFoundInterruption { + return responder(.next(request, response)) + } + responder(.error(error)) + } + } + } + } + } +} diff --git a/Sources/Slimane/Response.swift b/Sources/Slimane/Response.swift deleted file mode 100644 index c8b83ac..0000000 --- a/Sources/Slimane/Response.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Request.swift -// Slimane -// -// Created by Yuki Takei on 4/16/16. -// Copyright © 2016 MikeTOKYO. All rights reserved. -// - -extension Response { - - public init(redirect location: String) { - let headers: Headers = ["Location": location, "Cache-Control": "max-age=0, no-cache, no-store"] - self.init(status: .movedPermanently, headers: headers, body: []) - } - - var bodyLength: Int { - if case .buffer(let data) = body { - return data.bytes.count - } - return 0 - } - - public var isKeepAlive: Bool { - if version.minor == 0 { - return connection?.lowercased().index(of: "keep-alive") != nil - } - - return connection?.lowercased().index(of: "close") == nil - } -} diff --git a/Sources/Slimane/AsyncRoute.swift b/Sources/Slimane/Route/Route.swift similarity index 55% rename from Sources/Slimane/AsyncRoute.swift rename to Sources/Slimane/Route/Route.swift index 221bfb2..a614367 100644 --- a/Sources/Slimane/AsyncRoute.swift +++ b/Sources/Slimane/Route/Route.swift @@ -1,29 +1,23 @@ // -// AsyncRoute.swift +// Route.swift // Slimane // -// Created by Yuki Takei on 4/16/16. +// Created by Yuki Takei on 2016/10/05. // // -import POSIXRegex - -public protocol AsyncRoute: AsyncResponder { +protocol Route { var path: String { get } var regexp: Regex { get } var paramKeys: [String] { get } - var method: S4.Method { get } - var handler: AsyncResponder { get } - var middlewares: [AsyncMiddleware] { get } + var method: HTTPCore.Method { get } + var handler: Respond { get } + var middlewares: [Middleware] { get } + + func respond(_ request: Request, _ response: Response, _ chainer: (Chainer) -> Void) } -extension AsyncRoute { - public func respond(to request: Request, result: @escaping ((Void) throws -> Response) -> Void) { - result { - Response(status: .ok) - } - } - +extension Route { public func params(_ request: Request) -> [String: String] { guard let path = request.path else { return [:] @@ -41,15 +35,15 @@ extension AsyncRoute { } } -public struct BasicRouter: AsyncRoute { - public let path: String - public let paramKeys: [String] - public let regexp: Regex - public let method: S4.Method - public let handler: AsyncResponder - public let middlewares: [AsyncMiddleware] +struct BasicRoute: Route { + let path: String + let regexp: Regex + let method: HTTPCore.Method + let handler: Respond + let paramKeys: [String] + let middlewares: [Middleware] - public init(method: S4.Method, path: String, middlewares: [AsyncMiddleware] = [], handler: AsyncResponder) { + init(method: HTTPCore.Method, path: String, middlewares: [Middleware] = [], handler: @escaping Respond){ let parameterRegularExpression = try! Regex(pattern: ":([[:alnum:]_]+)") let pattern = parameterRegularExpression.replace(path, withTemplate: "([[:alnum:]_-]+)") @@ -60,5 +54,8 @@ public struct BasicRouter: AsyncRoute { self.middlewares = middlewares self.handler = handler } + + public func respond(_ request: Request, _ response: Response, _ chainer: (Chainer) -> Void) { + self.handler(request, response, chainer) + } } - diff --git a/Sources/Slimane/Router/Router.swift b/Sources/Slimane/Router/Router.swift new file mode 100644 index 0000000..1dd1bf8 --- /dev/null +++ b/Sources/Slimane/Router/Router.swift @@ -0,0 +1,28 @@ +// +// Router.swift +// Slimane +// +// Created by Yuki Takei on 2016/10/05. +// +// + +struct Router { + var routes = [Route]() + + public func matchedRoute(for request: Request) -> (Route, Request)? { + guard let path = request.path else { + return nil + } + + //let request = request + for route in routes { + if route.regexp.matches(path) && request.method == route.method { + var request = request + request.params = route.params(request) + return (route, request) + } + } + + return nil + } +} diff --git a/Sources/Slimane/Router/Slimane+Router.swift b/Sources/Slimane/Router/Slimane+Router.swift new file mode 100644 index 0000000..0ecc775 --- /dev/null +++ b/Sources/Slimane/Router/Slimane+Router.swift @@ -0,0 +1,48 @@ +// +// Router.swift +// Slimane +// +// Created by Yuki Takei on 4/16/16. +// +// + +public enum RoutingError: Error { + case routeNotFound(path: String) +} + +extension RoutingError: CustomStringConvertible { + public var description: String { + switch(self) { + case .routeNotFound(let path): + return "\(path) is not found" + } + } +} + +extension Slimane { + public func use(_ middleware: Middleware){ + self.middlewares.append(middleware) + } + + public func use(_ handler: @escaping Respond){ + self.middlewares.append(BasicMiddleware(handler)) + } + + public func use(_ method: HTTPCore.Method, _ path: String, _ handler: @escaping Respond){ + self.router.routes.append(BasicRoute(method: method, path: path, handler: handler)) + } + + public func use(_ method: HTTPCore.Method, _ path: String, _ middlewares: [Middleware], _ handler: @escaping Respond){ + self.router.routes.append(BasicRoute(method: method, path: path, middlewares: middlewares, handler: handler)) + } +} + +extension Slimane { + public func `catch`(_ handler: @escaping (Error, Request, Response, (Chainer) -> Void) -> Void){ + self.catchHandler = handler + } + + public func finally(_ handler: @escaping (Request, Response) -> Void){ + self.finallyHandler = handler + } +} diff --git a/Sources/Slimane/Slimane+Router.swift b/Sources/Slimane/Slimane+Router.swift deleted file mode 100644 index f5277f4..0000000 --- a/Sources/Slimane/Slimane+Router.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Router.swift -// Slimane -// -// Created by Yuki Takei on 4/16/16. -// -// - -public enum RoutingError: Error, CustomStringConvertible { - case routeNotFound(path: String) -} - -extension RoutingError { - public var description: String { - switch(self) { - case .routeNotFound(let path): - return "\(path) is not found" - } - } -} - -extension Slimane { - public func use(_ handler: AsyncMiddlewareHandler){ - middlewares.append(BasicAsyncMiddleware(handler)) - } - - public func use(_ handler: AsyncMiddleware){ - middlewares.append(handler) - } -} - -extension Slimane { - public func get(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - get(path, middlewares, handler: responder) - } - - public func get(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .get, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - -extension Slimane { - public func options(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - options(path, middlewares, handler: responder) - } - - public func options(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .options, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - -extension Slimane { - public func post(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - post(path, middlewares, handler: responder) - } - - public func post(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .post, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - -extension Slimane { - public func put(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - put(path, middlewares, handler: responder) - } - - public func put(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .put, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - -extension Slimane { - public func patch(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - patch(path, middlewares, handler: responder) - } - - public func patch(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .patch, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - -extension Slimane { - public func delete(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncRespond){ - let responder = BasicAsyncResponder(handler) - delete(path, middlewares, handler: responder) - } - - public func delete(_ path: String, _ middlewares: [AsyncMiddleware] = [], handler: AsyncResponder){ - let route = BasicRouter(method: .delete, path: path, middlewares: middlewares, handler: handler) - router.routes.append(route) - } -} - diff --git a/Sources/Slimane/Slimane+Server.swift b/Sources/Slimane/Slimane+Server.swift deleted file mode 100644 index 5aaa13a..0000000 --- a/Sources/Slimane/Slimane+Server.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Serve.swift -// Slimane -// -// Created by Yuki Takei on 4/16/16. -// -// - -extension Slimane { - - public func listen(loop: Loop = Loop.defaultLoop, host: String = "0.0.0.0", port: Int = 3000, errorHandler: @escaping (Error) -> () = { _ in }) throws { - let server = Skelton(loop: loop, ipcEnable: Cluster.isWorker) { [unowned self] in - do { - let (request, stream) = try $0() - self.dispatch(request, stream: stream) - } catch { - errorHandler(error) - } - } - - server.setNoDelay = self.setNodelay - server.keepAliveTimeout = self.keepAliveTimeout - server.backlog = self.backlog - - if Cluster.isMaster { - try server.bind(host: host, port: port) - } - try server.listen() - } - - private func dispatch(_ request: Request, stream: HTTPStream){ - let responder = BasicAsyncResponder { [unowned self] request, result in - if let route = self.router.match(request) { - var request = request - request.params = route.params(request) - route.middlewares.chain(to: route.handler).respond(to: request, result: result) - } else { - result { - self.errorHandler(RoutingError.routeNotFound(path: request.uri.path ?? "/")) - } - } - } - - self.middlewares.chain(to: responder).respond(to: request) { [unowned self] getResponse in - do { - let response = try getResponse() - if let responder = response.customResponder { - responder.respond(response) { getResponse in - do { - processStream(try getResponse(), request, stream) - } catch { - self.handleError(error, request, stream) - } - } - } else { - processStream(response, request, stream) - } - } catch { - self.handleError(error, request, stream) - } - } - } - - private func handleError(_ error: Error, _ request: Request, _ stream: HTTPStream){ - processStream(errorHandler(error), request, stream) - } -} - -private func processStream(_ response: Response, _ request: Request, _ stream: HTTPStream){ - var response = response - - response.headers["Date"] = Time().rfc1123 - - response.headers["Server"] = "Slimane" - - if response.contentLength == nil && !response.isChunkEncoded { - response.contentLength = response.bodyLength - } - - AsyncHTTPSerializer.ResponseSerializer(stream: stream).serialize(response) { result in - do { - try result() - if let didUpgradeAsync = response.didUpgradeAsync { - didUpgradeAsync(request, stream) - } - } catch { - do { try stream.close() } catch {} - } - } - - if !request.isKeepAlive && response.didUpgradeAsync == nil { - do { try stream.close() } catch {} - } -} diff --git a/Sources/Slimane/Slimane+Static.swift b/Sources/Slimane/Slimane+Static.swift deleted file mode 100644 index ca1f796..0000000 --- a/Sources/Slimane/Slimane+Static.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Static.swift -// Slimane -// -// Created by Yuki Takei on 4/19/16. -// -// - -import CLibUv - -public enum StaticMiddlewareError: Error, CustomStringConvertible { - case resourceNotFound(path: String) -} - -extension StaticMiddlewareError { - public var description: String { - switch(self) { - case .resourceNotFound(let resourceName): - return "\(resourceName) is not found" - } - } -} - -extension Slimane { - public struct Static: AsyncMiddleware { - - let root: String - - let ignoreNotFoundInterruption: Bool - - public init(root: String, ignoreNotFoundInterruption: Bool = false){ - self.root = root - self.ignoreNotFoundInterruption = ignoreNotFoundInterruption - } - - public func respond(to request: Request, chainingTo next: AsyncResponder, result: @escaping ((Void) throws -> Response) -> Void) { - guard let path = request.path , let ext = path.split(separator: ".").last, let mediaType = mediaType(forFileExtension: ext) else { - return next.respond(to: request, result: result) - } - - FS.readFile(root + path) { getData in - do { - var response = Response(body: try getData()) - response.contentType = mediaType - result { - response - } - } catch UVError.rawUvError(let code) { - let e: Error - if code == -2 { - e = StaticMiddlewareError.resourceNotFound(path: path) - } else { - e = UVError.rawUvError(code: code) - } - - result { - throw e - } - } catch { - if self.ignoreNotFoundInterruption { - return next.respond(to: request, result: result) - } - - result { - throw error - } - } - } - } - } -} diff --git a/Sources/Slimane/Slimane.swift b/Sources/Slimane/Slimane.swift index 7337432..056c582 100644 --- a/Sources/Slimane/Slimane.swift +++ b/Sources/Slimane/Slimane.swift @@ -6,43 +6,22 @@ // Copyright © 2016 MikeTOKYO. All rights reserved. // -@_exported import Time @_exported import Skelton -@_exported import S4 -@_exported import C7 -@_exported import AsyncResponderConvertible -@_exported import AsyncHTTPSerializer -@_exported import HTTPUpgradeAsync public class Slimane { - internal var middlewares: [AsyncMiddleware] = [] + internal var middlewares: [Middleware] = [] - internal var router: Router + internal var router = Router() public var setNodelay = false public var keepAliveTimeout: UInt = 15 public var backlog: UInt = 1024 + + var catchHandler: (Error, Request, Response, (Chainer) -> Void) -> Void = { _ in } + + var finallyHandler: (Request, Response) -> Void = { _ in } - public var errorHandler: (Error) -> Response = defaultErrorHandler - - public init(){ - self.router = Router { _, result in - result { Response() } - } - } -} - -func defaultErrorHandler(_ error: Error) -> Response { - let response: Response - switch error { - case RoutingError.routeNotFound: - response = Response(status: .notFound, body: "\(error)") - case StaticMiddlewareError.resourceNotFound: - response = Response(status: .notFound, body: "\(error)") - default: - response = Response(status: .internalServerError, body: "\(error)") - } - return response + public init(){} } diff --git a/Sources/Slimane/Util/Regex.swift b/Sources/Slimane/Util/Regex.swift new file mode 100644 index 0000000..8cb49b7 --- /dev/null +++ b/Sources/Slimane/Util/Regex.swift @@ -0,0 +1,154 @@ +// +// Regex.swift +// Slimane +// +// Created by Yuki Takei on 2016/10/04. +// +// + +#if os(Linux) + @_exported import Glibc +#else + @_exported import Darwin.C +#endif + +struct RegexError: Error { + let description: String + + static func error(from result: Int32, preg: regex_t) -> RegexError { + var preg = preg + var buffer = [Int8](repeating: 0, count: Int(BUFSIZ)) + regerror(result, &preg, &buffer, buffer.count) + let description = String(validatingUTF8: buffer)! + return RegexError(description: description) + } +} + +public final class Regex { + public struct RegexOptions: OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let basic = RegexOptions(rawValue: 0) + public static let extended = RegexOptions(rawValue: 1) + public static let caseInsensitive = RegexOptions(rawValue: 2) + public static let resultOnly = RegexOptions(rawValue: 8) + public static let newLineSensitive = RegexOptions(rawValue: 4) + } + + public struct MatchOptions: OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let FirstCharacterNotAtBeginningOfLine = MatchOptions(rawValue: REG_NOTBOL) + public static let LastCharacterNotAtEndOfLine = MatchOptions(rawValue: REG_NOTEOL) + } + + var preg = regex_t() + + public init(pattern: String, options: RegexOptions = .extended) throws { + let result = regcomp(&preg, pattern, options.rawValue) + + if result != 0 { + throw RegexError.error(from: result, preg: preg) + } + } + + deinit { + regfree(&preg) + } + + public func matches(_ string: String, options: MatchOptions = []) -> Bool { + var regexMatches = [regmatch_t](repeating: regmatch_t(), count: 1) + let result = regexec(&preg, string, regexMatches.count, ®exMatches, options.rawValue) + + if result == 1 { + return false + } + + return true + } + + public func groups(_ string: String, options: MatchOptions = []) -> [String] { + var string = string + let maxMatches = 10 + var groups = [String]() + + while true { + var regexMatches = [regmatch_t](repeating: regmatch_t(), count: maxMatches) + let result = regexec(&preg, string, regexMatches.count, ®exMatches, options.rawValue) + + if result == 1 { + break + } + + var j = 1 + + while regexMatches[j].rm_so != -1 { + let start = Int(regexMatches[j].rm_so) + let end = Int(regexMatches[j].rm_eo) + let startIndex = string.index(string.startIndex, offsetBy: start) + let endIndex = string.index(string.startIndex, offsetBy: end) + let match = string[startIndex ..< endIndex] + groups.append(match) + j += 1 + } + + let offset = Int(regexMatches[0].rm_eo) + let startIndex = string.utf8.index(string.utf8.startIndex, offsetBy: offset) + if let offsetString = String(string.utf8[startIndex ..< string.utf8.endIndex]) { + string = offsetString + } else { + break + } + } + + return groups + } + + public func replace(_ string: String, withTemplate template: String, options: MatchOptions = []) -> String { + var string = string + let maxMatches = 10 + var totalReplacedString: String = "" + + while true { + var regexMatches = [regmatch_t](repeating: regmatch_t(), count: maxMatches) + let result = regexec(&preg, string, regexMatches.count, ®exMatches, options.rawValue) + + if result == 1 { + break + } + + let start = Int(regexMatches[0].rm_so) + let end = Int(regexMatches[0].rm_eo) + + var replacedStringArray = Array(string.utf8) + let templateArray = Array(template.utf8) + replacedStringArray.replaceSubrange(start ..< end, with: templateArray) + + guard let _replacedString = try? String(data: Data(replacedStringArray)) else { + break + } + + var replacedString = _replacedString + + let templateDelta = template.utf8.count - (end - start) + let offset = Int(end + templateDelta) + let templateDeltaIndex = replacedString.utf8.index(replacedString.utf8.startIndex, offsetBy: offset) + + replacedString = String(describing: replacedString.utf8[replacedString.utf8.startIndex ..< templateDeltaIndex]) + + totalReplacedString += replacedString + let startIndex = string.utf8.index(string.utf8.startIndex, offsetBy: end) + string = String(describing: string.utf8[startIndex ..< string.utf8.endIndex]) + } + + return totalReplacedString + string + } +} diff --git a/Sources/WebAppExample/main.swift b/Sources/WebAppExample/main.swift new file mode 100644 index 0000000..89bec7c --- /dev/null +++ b/Sources/WebAppExample/main.swift @@ -0,0 +1,53 @@ +// +// main.swift +// Slimane +// +// Created by Yuki Takei on 2016/10/07. +// +// + +import Slimane + +let app = Slimane() + +app.use(Slimane.Static(root: "\(Process.cwd)")) +app.use(BodyParser.JSON()) + +app.use(.post, "/json") { request, response, responder in + var response = response + response.status(.created) + response.json(request.json ?? ["message": "non json body"]) + responder(.respond(response)) +} + +app.use(.get, "/") { request, response, responder in + var response = response + response.text("Slimane Example AppWelcome to Slimane!") + responder(.respond(response)) +} + +app.`catch` { error, request, response, responder in + var response = response + switch error { + case RoutingError.routeNotFound: + response.status(.notFound) + response.text("\(error)") + + case StaticMiddlewareError.resourceNotFound: + response.status(.notFound) + response.text("\(error)") + + default: + response.status(.internalServerError) + response.text("\(error)") + } + + responder(.respond(response)) +} + +app.finally { request, response in + print("\(request.method) \(request.path ?? "/") \(response.status.statusCode)") +} + +print("Started HTTP server at 0.0.0.0:3000") +try! app.listen() From 6b96475e50dea913829fefed36456c9ac2730e38 Mon Sep 17 00:00:00 2001 From: noppoman Date: Fri, 7 Oct 2016 19:44:49 +0900 Subject: [PATCH 2/7] Update README.md --- README.md | 243 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 127 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 70677cb..8af8f98 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ Slimane is an express inspired web framework for Swift that works on OSX and Ubu - [x] 100% Asynchronous - [x] Unopinionated and Minimalist -- [x] Adopts [Open Swift](https://github.com/open-swift) +- [x] Incredible Performance + +## Benchmark -### Programming Guid Getting ready ## Slimane Project Page 🎉 @@ -26,9 +27,6 @@ The entire Slimane code base is licensed under MIT. By contributing to Slimane y ### Install Guide [Here is an install guides for each operating systems](https://github.com/noppoMan/Slimane/wiki/Install-Guide) -### Documentation -[Here is a Documentation for Slimane.](https://github.com/noppoMan/Slimane/wiki) - ## Usage Starting the application takes slight lines. @@ -38,10 +36,10 @@ import Slimane let app = Slimane() -app.get("/") { req, responder in - responder { - Response(body: "Welcome Slimane!") - } +app.use(.get, "/") { request, response, responder in + var response = response + response.text("Welcome to Slimane!") + responder(.respond(response)) } try! app.listen() @@ -64,10 +62,10 @@ slimane run ## Routing ```swift -app.get("/articles/:id") { req, responder in - responder { - Response(body: "Article ID is: \(req.params["id"]!)") - } +app.use(.get, "/articles/:id") { request, response, responder in + var response = response + response.text("Article ID is: \(req.params["id"]!)") + responder(.respond(response)) } ``` @@ -78,6 +76,7 @@ app.get("/articles/:id") { req, responder in * put * patch * delete +* other(method: String) ## Middlewares @@ -86,39 +85,33 @@ Middleware is functions that have access to the http request, the http response, ### Handy ```swift -app.use { req, next, result in - print("[\(Suv.Time())] \(req.uri.path ?? "/")") - next.respond(to: req, result: result) +app.use { request, response, responder in + do { + try doSomething() + responder(.next(request, response)) // Chaining to the next middleware or route + } catch { + responder(.error(error)) // Go to `catch` handler + } } ``` -### AsyncMiddleware +### Middleware Protocol ```swift -struct FooMiddleware: AsyncMiddleware { - func respond(to request: Request, chainingTo next: AsyncResponder, result: (Void throws -> Response) -> Void) { - do { - var request = request - let foo = try throwableFoo() - request.foo = foo - next.respond(to: request, result: result) // Chain the next middleware - } catch { - // Respond to the content immediately. - result { - Response(status: .internalServerError, body: "\(error)") - } - } +struct FooMiddleware: Middleware { + func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { + do { + try doSomething() + responder(.next(request, response)) // Chaining to the next middleware or route + } catch { + responder(.error(error)) // Go to `catch` handler + } } } app.use(FooMiddleware()) ``` -## Request/Response - -We are using S4.Request and S4.Response -See more detail, please visit https://github.com/open-swift/S4 - ## Static Files/Assets Just register the `Slimane.Static()` into middleware chains @@ -136,8 +129,6 @@ request.cookies is Readonly. req.cookies["session-id"] ``` -**Cookie** is declared in HTTP.Cookie. See more to visit https://github.com/slimane-swift/HTTP - #### response.cookies: `Set` response.cookies is Writable. @@ -146,8 +137,6 @@ response.cookies is Writable. let setCookie = AttributedCookie(....) res.cookies = Set ``` -**AttributedCookie** is declared in HTTP.AttributedCookie. See more to visit https://github.com/slimane-swift/HTTP - ## Session @@ -170,15 +159,14 @@ let sesConf = SessionConfig( // Enable to use session in Slimane app.use(SessionMiddleware(conf: sesConf)) -app.get("/") { req, responder +app.use(.get, "/") { request, response, responder in + var request = request // set data into the session - req.session["foo"] = "bar" + request.session["foo"] = "bar" - req.session.id // show session id + request.session.id // show session id - responder { - Response() - } + response(.respond(response)) } ``` @@ -191,45 +179,44 @@ app.get("/") { req, responder Register BodyParser into the middleware chains. -```swift -app.use(BodyParser()) -``` - -#### request.json `Zewo.JSON` +#### request.json `SwiftyJSON.JSON` Can get parsed json data throgh the req.json when content-type is `application/json` ```swift -req.json?["foo"] +app.use(BodyParser.JSON()) ``` -#### request.formData `[String: String]` +```swift +request.json?["foo"] +``` + +#### request.formData `URLEncodedForm` Can get parsed form data throgh the req.formData when content-type is `application/x-www-form-urlencoded` ```swift -req.formData?["foo"] +app.use(BodyParser.URLEncoded()) +``` + +```swift +request.formData?["foo"] ``` ## Views/Template Engines * Add the [Render](https://github.com/slimane-swift/Render) module into the Package.swift * Add the [MustacheViewEngine](https://github.com/slimane-swift/MustacheViewEngine) module into the Package.swift -Then, You can use render function in Slimane. and pass the render object to the `custom` label for Response initializer. +Then, You can use render function in Slimane with following code. ```swift -app.get("/render") { req, responder in - responder { - let render = Render(engine: MustacheViewEngine(templateData: ["foo": "bar"]), path "index") - Response(custom: render) - } +app.get("/render") { request, response, responder in + var response = response + response.render(MustacheViewEngine("index", templateData: ["foo": "bar"])) + responder(.respond(response)) } ``` -## Create your own ViewEngine - -Getting ready - ## Working with Cluster A single instance of Slimane runs in a single thread. To take advantage of multi-core systems the user will sometimes want to launch a cluster of Slimane processes to handle the load. @@ -247,10 +234,10 @@ if Cluster.isMaster { try! Slimane().listen() } else { let app = Slimane() - app.get("/") { req, responder in - responder { - Response(body: "Hello! I'm \(CommandLine.pid)") - } + app.get("/") { request, response, responder in + var response = response + response.text("Hello! process id is \(Process.pid)") + responder(.respond(response)) } try! app.listen() @@ -259,8 +246,6 @@ if Cluster.isMaster { ## IPC between Master and Worker Processes -Inter process message between master and workers - ### On Master ```swift var worker = try! Cluster.fork(silent: false) @@ -269,7 +254,7 @@ var worker = try! Cluster.fork(silent: false) worker.send(.Message("message from master")) // Receive event from the worker -worker.onIPC { event in +worker.onEvent { event in if case .message(let str) = event { print(str) } @@ -290,7 +275,7 @@ worker.onIPC { event in ```swift // Receive event from the master -Process.onIPC { event in +Process.onEvent { event in if case .message(let str) = event { print(str) } @@ -300,10 +285,70 @@ Process.onIPC { event in Process.send(.message("Hey!")) ``` -## Respond to the Streaming Content +## Life Cycle + +## Handling Errors + +You can catch the error that are emitted from the middleware or the route with `app.catch` handler. + +```swift +let app = Slimane() + +app.use { request, response, responder in + responder(.error(FooError)) +} + +app.`catch` { error, request, response, responder in + var response = response + switch error { + case RoutingError.routeNotFound: + response.status(.notFound) + response.text("\(error)") + + case StaticMiddlewareError.resourceNotFound: + response.status(.notFound) + response.text("\(error)") + + default: + response.status(.internalServerError) + response.text("\(error)") // fooError + } + + responder(.respond(response)) +} + +try! app.listen() +``` + +## Finalization + +After responded to the content, You can process finalization for the request with `app.finally` block. +Here is a logging example for the request. + +```swift + +let app = Slimane() + +app.use(.get, "/") { request, response, responder in + var response = response + response.text("Welcome to Slimane!") + responder(.respond(response)) +} + +app.finally { request, response in + print("\(request.method) \(request.path ?? "/") \(response.status.statusCode)") // GET / 200 +} + +try! app.listen() +``` + -You can respond to the streaming content with `Body.asyncSender` -Here is an example for websocket response with [WS](https://github.com/slimane-swift/WS) +## Extras + +### WebSocket + +You can respond to the streaming content with `Body.writer` +Here is an example for processing streaming content with [WS](https://github.com/slimane-swift/WS) ```swift import WS @@ -313,22 +358,15 @@ let app = Slimane() let wsServer = WebSocketServer { socket, request in socket.onText { text in - print(text) + socket.send("PONG") } } app.use(wsServer) -app.get("/") { req, responder in - responder { - Response(body: "html text here....") - } -} - try! app.listen() ``` -## Extras ### Working with blocking functions We have `Process.qwork` to run blocking functions in a separated thread. @@ -337,7 +375,7 @@ It allows potentially any third-party libraries to be used with the event-loop p ```swift let onThread = { ctx in - do + do ctx.storage["result"] = try blokingOperation() } catch { ctx.storage["error"] = error @@ -349,7 +387,7 @@ let onFinish = { ctx in print(error) return } - + print(ctx.storage["result"]) } @@ -370,22 +408,22 @@ extension DB { func execute(sql: String) -> Promise { return Promise { resolve, reject in let onThread = { ctx in - do + do ctx.storage["result"] = try blockingSqlQuery(sql) } catch { ctx.storage["error"] = error } } - + let onFinish = { ctx in if let error = ctx.storage["error"] as? Error { reject(error) return } - + resolve(ctx.storage["result"] as! FooResult) } - + Process.qwork(onThread: onThread, onFinish: onFinish) } } @@ -405,33 +443,6 @@ db.execute("insert into users (id, name) values (1, 'jack')").then { ``` -## Handling Errors - -Easy to override Default Error Handler with replace `app.errorHandler` to your costume handler. -All of the errors that occurred in Slimane's lifecycles are passed as first argument of errorHandler even RouteNotFound. - -```swift -let app = Slimane() - -app.errorHandler = myErrorHandler - -func myErrorHandler(error: ErrorProtocol) -> Response { - let response: Response - switch error { - case Costume.invalidPrivilegeError - response = Response(status: .forbidden, body: "Forbidden") - case Costume.resourceNotFoundError(let name) - response = Response(status: .notFound, body: "\(name) is not found") - default: - response = Response(status: .badRequest, body: "\(error)") - } - - return response -} - -try! app.listen() -``` - ## Package.swift ```swift @@ -440,7 +451,7 @@ import PackageDescription let package = Package( name: "MySlimaneApp", dependencies: [ - .Package(url: "https://github.com/noppoMan/Slimane.git", majorVersion: 0, minor: 8), + .Package(url: "https://github.com/noppoMan/Slimane.git", majorVersion: 0, minor: 9), ] ) ``` From 2dcbc6cf7bc5ab66901da718dec6ab8e5fbc6303 Mon Sep 17 00:00:00 2001 From: noppoman Date: Tue, 11 Oct 2016 00:40:21 +0900 Subject: [PATCH 3/7] add make xcode --- makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/makefile b/makefile index 21b356c..e0ce1d2 100644 --- a/makefile +++ b/makefile @@ -25,3 +25,6 @@ release: test: $(SWIFT) test $(BUILDOPTS) + +xcode: + $(SWIFT) package generate-xcodeproj $(BUILDOPTS) From bffedea3e17592181cd3dae74c080005341bcfe5 Mon Sep 17 00:00:00 2001 From: Yuki Takei Date: Tue, 11 Oct 2016 03:38:12 +0900 Subject: [PATCH 4/7] Update README.md --- README.md | 418 +----------------------------------------------------- 1 file changed, 4 insertions(+), 414 deletions(-) diff --git a/README.md b/README.md index 8af8f98..f76a47b 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,6 @@ https://github.com/slimane-swift ## Community The entire Slimane code base is licensed under MIT. By contributing to Slimane you are contributing to an open and engaged community of brilliant Swift programmers. Join us on [Slack](https://slimane-swift-slackin.herokuapp.com/) to get to know us! -## Getting Started - -### Install Guide -[Here is an install guides for each operating systems](https://github.com/noppoMan/Slimane/wiki/Install-Guide) - ## Usage Starting the application takes slight lines. @@ -45,416 +40,11 @@ app.use(.get, "/") { request, response, responder in try! app.listen() ``` -### Generate a new Slimane App via slimane-cli(Requires Node.js v4 or later) -```sh -npm i -g slimane-cli -``` - -```sh -slimane new YourAppName -cd YourAppName -slimane build -slimane run -``` - -**That's it!** - -## Routing - -```swift -app.use(.get, "/articles/:id") { request, response, responder in - var response = response - response.text("Article ID is: \(req.params["id"]!)") - responder(.respond(response)) -} -``` - -#### Methods -* get -* options -* post -* put -* patch -* delete -* other(method: String) - - -## Middlewares -Middleware is functions that have access to the http request, the http response, and the next function in the application' s request-response cycle. - -### Handy - -```swift -app.use { request, response, responder in - do { - try doSomething() - responder(.next(request, response)) // Chaining to the next middleware or route - } catch { - responder(.error(error)) // Go to `catch` handler - } -} -``` - -### Middleware Protocol - -```swift -struct FooMiddleware: Middleware { - func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { - do { - try doSomething() - responder(.next(request, response)) // Chaining to the next middleware or route - } catch { - responder(.error(error)) // Go to `catch` handler - } - } -} - -app.use(FooMiddleware()) -``` - -## Static Files/Assets - -Just register the `Slimane.Static()` into middleware chains - -```swift -app.use(Slimane.Static(root: "/path/to/your/public")) -``` - -## Cookie - -#### request.cookies: `Set` - -request.cookies is Readonly. -```swift -req.cookies["session-id"] -``` - -#### response.cookies: `Set` - -response.cookies is Writable. - -```swift -let setCookie = AttributedCookie(....) -res.cookies = Set -``` - -## Session - -Register SessionMiddleware into the middleware chains. -See more detail for SessionMiddleware to visit https://github.com/slimane-swift/SessionMiddleware - -```swift -import Slimane -import SessionMiddleware - -let app = Slimane() - -// SessionConfig -let sesConf = SessionConfig( - secret: "my-secret-value", - expires: 180, - HTTPOnly: true -) - -// Enable to use session in Slimane -app.use(SessionMiddleware(conf: sesConf)) - -app.use(.get, "/") { request, response, responder in - var request = request - // set data into the session - request.session["foo"] = "bar" - - request.session.id // show session id - - response(.respond(response)) -} -``` - -### Available Session Stores -* MemoryStore -* SessionRedisStore - - -## Body Data - -Register BodyParser into the middleware chains. - -#### request.json `SwiftyJSON.JSON` - -Can get parsed json data throgh the req.json when content-type is `application/json` - -```swift -app.use(BodyParser.JSON()) -``` - -```swift -request.json?["foo"] -``` - -#### request.formData `URLEncodedForm` - -Can get parsed form data throgh the req.formData when content-type is `application/x-www-form-urlencoded` - -```swift -app.use(BodyParser.URLEncoded()) -``` - -```swift -request.formData?["foo"] -``` - -## Views/Template Engines -* Add the [Render](https://github.com/slimane-swift/Render) module into the Package.swift -* Add the [MustacheViewEngine](https://github.com/slimane-swift/MustacheViewEngine) module into the Package.swift - -Then, You can use render function in Slimane with following code. - -```swift -app.get("/render") { request, response, responder in - var response = response - response.render(MustacheViewEngine("index", templateData: ["foo": "bar"])) - responder(.respond(response)) -} -``` - -## Working with Cluster - -A single instance of Slimane runs in a single thread. To take advantage of multi-core systems the user will sometimes want to launch a cluster of Slimane processes to handle the load. - - -Here is an easy example for working with Suv.Cluster - -```swift -// For Cluster app -if Cluster.isMaster { - for _ in 0.. Promise { - return Promise { resolve, reject in - let onThread = { ctx in - do - ctx.storage["result"] = try blockingSqlQuery(sql) - } catch { - ctx.storage["error"] = error - } - } - - let onFinish = { ctx in - if let error = ctx.storage["error"] as? Error { - reject(error) - return - } - - resolve(ctx.storage["result"] as! FooResult) - } - - Process.qwork(onThread: onThread, onFinish: onFinish) - } - } -} - -let db = DB(host: "localhost") - -db.execute("insert into users (id, name) values (1, 'jack')").then { - print($0) -} -.`catch` { - print($0) -} -.finally { - print("Done") -} -``` - - -## Package.swift - -```swift -import PackageDescription +## Getting Started +[Installation Guide](https://github.com/noppoMan/Slimane/wiki) -let package = Package( - name: "MySlimaneApp", - dependencies: [ - .Package(url: "https://github.com/noppoMan/Slimane.git", majorVersion: 0, minor: 9), - ] -) -``` +## Documentation +[Documentation Page is here](https://github.com/noppoMan/Slimane/wiki) ## License From d2cefca215d38c66d1c3087fe19e940345239c29 Mon Sep 17 00:00:00 2001 From: noppoman Date: Wed, 12 Oct 2016 00:53:32 +0900 Subject: [PATCH 5/7] support customResponder --- Sources/Slimane/HTTP/Slimane+Server.swift | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Sources/Slimane/HTTP/Slimane+Server.swift b/Sources/Slimane/HTTP/Slimane+Server.swift index 0350178..11c9b28 100644 --- a/Sources/Slimane/HTTP/Slimane+Server.swift +++ b/Sources/Slimane/HTTP/Slimane+Server.swift @@ -42,12 +42,14 @@ extension Slimane { if let (route, request) = self.router.matchedRoute(for: request) { route.middlewares.chain(request: request, response: response) { [unowned self] chainer in switch chainer { + // middleware respond case .respond(let response): self.respond(request, response, stream) case .next(let request, let response): route.respond(request, response) { chainer in switch chainer { + // route respond case .respond(let response): self.respond(request, response, stream) @@ -84,20 +86,46 @@ extension Slimane { case .next(_): response.status(.internalServerError) response.text("\(MiddlewareError.noNextMiddleware)") - self.respond(request, response, stream) + self.processStream(request, response, stream) case .error(_): response.status(.internalServerError) response.text("Something went wrong.") - self.respond(request, response, stream) + self.processStream(request, response, stream) } } } private func respond(_ request: HTTPCore.Request, _ response: HTTPCore.Response, _ stream: DuplexStream){ + if let responder = response.customResponder { + responder.respond(request: request, response: response) { [unowned self] chainer in + switch chainer { + case .respond(let response): + self.processStream(request, response, stream) + + case .next(_): + var response = response + response.status(.internalServerError) + response.text("\(MiddlewareError.noNextMiddleware)") + self.processStream(request, response, stream) + + case .error(let error): + var response = response + response.status(.internalServerError) + response.text("\(error)") + self.processStream(request, response, stream) + } + } + } else { + self.processStream(request, response, stream) + } + } + + private func processStream(_ request: HTTPCore.Request, _ response: HTTPCore.Response, _ stream: DuplexStream){ var response = response response.headers["Server"] = "Slimane" + if response.contentType == nil { response.contentType = mediaType(forFileExtension: "html")! } From d092f1accd4aac99b36269deac4e0fce020c8eab Mon Sep 17 00:00:00 2001 From: noppoman Date: Wed, 12 Oct 2016 03:16:46 +0900 Subject: [PATCH 6/7] remove argument labels from Responder --- Sources/Slimane/HTTP/Slimane+Server.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Slimane/HTTP/Slimane+Server.swift b/Sources/Slimane/HTTP/Slimane+Server.swift index 11c9b28..5971396 100644 --- a/Sources/Slimane/HTTP/Slimane+Server.swift +++ b/Sources/Slimane/HTTP/Slimane+Server.swift @@ -98,7 +98,7 @@ extension Slimane { private func respond(_ request: HTTPCore.Request, _ response: HTTPCore.Response, _ stream: DuplexStream){ if let responder = response.customResponder { - responder.respond(request: request, response: response) { [unowned self] chainer in + responder.respond(request, response) { [unowned self] chainer in switch chainer { case .respond(let response): self.processStream(request, response, stream) From 1cbce79281e907d723867c63991fbe3d885881de Mon Sep 17 00:00:00 2001 From: noppoman Date: Sat, 29 Oct 2016 18:34:21 +0900 Subject: [PATCH 7/7] Route protocol confirm Responder protocol --- Sources/Slimane/Route/Route.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/Slimane/Route/Route.swift b/Sources/Slimane/Route/Route.swift index a614367..be2b8db 100644 --- a/Sources/Slimane/Route/Route.swift +++ b/Sources/Slimane/Route/Route.swift @@ -6,15 +6,13 @@ // // -protocol Route { +protocol Route: Responder { var path: String { get } var regexp: Regex { get } var paramKeys: [String] { get } var method: HTTPCore.Method { get } var handler: Respond { get } var middlewares: [Middleware] { get } - - func respond(_ request: Request, _ response: Response, _ chainer: (Chainer) -> Void) } extension Route { @@ -55,7 +53,7 @@ struct BasicRoute: Route { self.handler = handler } - public func respond(_ request: Request, _ response: Response, _ chainer: (Chainer) -> Void) { - self.handler(request, response, chainer) + func respond(_ request: Request, _ response: Response, _ responder: @escaping (Chainer) -> Void) { + self.handler(request, response, responder) } }