├── .gitignore ├── .travis.yml ├── Demo ├── Info.plist ├── main.swift ├── public │ ├── css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.min.css │ │ ├── main.css │ │ └── xcode.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── js │ │ ├── bootstrap.min.js │ │ ├── highlight.pack.js │ │ └── jquery.min.js │ └── logo.png └── views │ ├── 404.stencil │ ├── code │ ├── 404.stencil │ ├── echo-param.stencil │ ├── echo.stencil │ ├── error.stencil │ ├── factorial.stencil │ ├── hello-user.stencil │ ├── hello.stencil │ ├── query.stencil │ ├── redirect.stencil │ └── render.stencil │ ├── colored.stencil │ ├── css.stencil │ ├── error-recovered.stencil │ ├── hello.stencil │ ├── index.stencil │ ├── js.stencil │ ├── mustache.mustache │ └── test.stencil ├── Express ├── Action.swift ├── AnyContent+FormUrlEncoded.swift ├── AnyContent+JSON.swift ├── AnyContent+MultipartFormData.swift ├── AnyContent+XML.swift ├── AnyContent.swift ├── CacheControl.swift ├── Content.swift ├── EVHTP.swift ├── ErrorHandler.swift ├── Errors.swift ├── EventExecutionContext.swift ├── ExecutionContext.swift ├── Express.swift ├── ExpressServer.swift ├── ExpressSugar.swift ├── Functional.swift ├── Headers.swift ├── HttpMethod.swift ├── HttpServer.swift ├── Info.plist ├── JsonView.swift ├── MIME.swift ├── MustacheViewEngine.swift ├── RegexUrlMatcher.swift ├── Request.swift ├── Response.swift ├── Router.swift ├── Server.swift ├── Static.swift ├── StatusCode.swift ├── StencilViewEngine.swift ├── Streams+Headers.swift ├── Streams.swift ├── Transaction.swift ├── UrlMatcher.swift ├── Utils.swift ├── View.swift ├── ViewEngine.swift └── Views.swift ├── LICENSE ├── LICENSE.LESSER ├── Package.swift ├── README.md ├── doc ├── gettingstarted │ ├── buildrun.md │ ├── commandline.md │ ├── errorhandling.md │ ├── helloexpress.md │ ├── installing.md │ ├── routing.md │ └── static.md └── index.md └── logo-full.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | *.xcworkspace 3 | *.xcodeproj 4 | xcuserdata 5 | 6 | # Carthage 7 | Cartfile.resolved 8 | Carthage 9 | 10 | # SPM 11 | .build 12 | Package.pins 13 | Packages 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - MODULE_NAME=Express 4 | matrix: 5 | include: 6 | - script: 7 | - xcodebuild build -project $MODULE_NAME.xcodeproj -scheme $MODULE_NAME 8 | os: osx 9 | osx_image: xcode7.2 10 | language: objective-c 11 | before_install: 12 | - brew update 13 | - brew unlink carthage 14 | - brew install carthage 15 | - brew link carthage 16 | - brew tap crossroadlabs/tap 17 | - brew install libevhtp --without-oniguruma --with-shared 18 | before_script: 19 | # bootstrap the dependencies for the project 20 | # you can remove if you don't have dependencies 21 | - carthage bootstrap --platform osx 22 | before_deploy: 23 | - carthage build --no-skip-current --platform osx 24 | - carthage archive $MODULE_NAME 25 | deploy: 26 | provider: releases 27 | api_key: 28 | secure: NxWvEe4nqcI99oKbtcFtmpz3AVkQiC+4kFu4OcCusAoKimMxx3WJ4+B6anDLG5dj/5rhsl4rQdKr/WIneHodyPrIFQPsRLUV2KFxgd+vbKrXLRRw+sZb//r0j89E7tCEUPdModIKWN4OCR7UWHvZ5syVQFfsP4MH1r+OJcBiewVVVPhgXzvndkxVlgXonyrJ++NAW/g+jO+CYNU9Y3KkyK7BaPMw6/oSC2Ly3A3zU1El++MFz+ew6YDGxiEiw7y20q7/qTT4pdl2HWem1P04UwPyJ2fUw/nD0C+fvX6tnFYX0s9p9VPSnrWXkhSHKT9IGlr8sveRPuQIaJcXcbkMHF8rnCmMpJJ63+YeXV88krW0q2ZyVpSodh/NaJ76PqbXij9WBSZ8AmNJsBlBsjhAgPCfZZkJ3vjrSSH2XP4k0ICLI8sxYmELXzR0MU1LaexE/4KKMDUcl4zuHWQBTJTMvtDyl+AJk+GdPBSGEpCQvHjXAwuW9OAz/sqZrLQy8eQJmji8DVhMJYlCK+krBQFZIekA6Wun5VTmATrtcwd6qwXyMOYeoblh3308zhzgyjBQwfJVpWxLLqB+dyQNLEOYhMcCcTwWNMvz7RYbEMcplkIu5azuUL400pPk2g2wp6U522vsAmH7bra3+VyA0qYG0ckLiwBaBKVCxD7EzkAy5CA= 29 | file: $MODULE_NAME.framework.zip 30 | skip_cleanup: true 31 | on: 32 | repo: crossroadlabs/Express 33 | tags: true 34 | - script: 35 | - ./build 36 | sudo: required 37 | dist: trusty 38 | language: generic 39 | before_install: 40 | # install original swift distribution 41 | - wget -q -O - https://swift.org/keys/all-keys.asc | gpg --import - 42 | - cd .. 43 | - export SWIFT_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a 44 | - wget https://swift.org/builds/development/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz 45 | - tar xzf $SWIFT_VERSION-ubuntu14.04.tar.gz 46 | - export PATH="${PWD}/${SWIFT_VERSION}-ubuntu14.04/usr/bin:${PATH}" 47 | # express dependencies 48 | - sudo add-apt-repository ppa:swiftexpress/swiftexpress --yes 49 | - sudo apt-get update 50 | - sudo apt-get install libevhtp-dev libevent-dev libssl-dev 51 | # get back home 52 | - cd $MODULE_NAME 53 | # get crossroad build script 54 | - wget https://raw.githubusercontent.com/crossroadlabs/utils/master/build 55 | - chmod +x build 56 | notifications: 57 | email: false 58 | 59 | -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015-2016 Daniel Leping (dileping). All rights reserved. 29 | 30 | 31 | -------------------------------------------------------------------------------- /Demo/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // SwiftExpress 4 | // 5 | // Created by Daniel Leping on 12/16/15. 6 | // Copyright © 2015-2016 Daniel Leping (dileping) 7 | // 8 | 9 | import Foundation 10 | import Express 11 | import Future 12 | 13 | let app = express() 14 | 15 | //always enable for production 16 | //app.views.cache = true 17 | 18 | app.views.register(JsonView()) 19 | app.views.register(MustacheViewEngine()) 20 | app.views.register(StencilViewEngine()) 21 | 22 | app.get(path: "/echo") { request in 23 | .ok(request.query["call"]?.first) 24 | } 25 | 26 | enum NastyError : Error { 27 | case recoverable 28 | case fatal(reason:String) 29 | } 30 | 31 | app.get(path: "/error/recovered") { request in 32 | .render(view: "error-recovered", context: [String:Any]()) 33 | } 34 | 35 | app.get(path: "/error/:fatal?") { (request:Request) throws -> Action in 36 | guard let fatal = request.params["fatal"] else { 37 | throw NastyError.recoverable 38 | } 39 | 40 | throw NastyError.fatal(reason: fatal) 41 | } 42 | 43 | app.errorHandler.register { (e:NastyError) in 44 | switch e { 45 | case .recoverable: 46 | return .redirect(url: "/error/recovered") 47 | case .fatal(let reason): 48 | let content = AnyContent(str: "Unrecoverable nasty error happened. Reason: " + reason) 49 | return .response(status: .InternalServerError, content: content) 50 | } 51 | } 52 | 53 | /// Custom page not found error handler 54 | app.errorHandler.register { (e:ExpressError) in 55 | switch e { 56 | case .PageNotFound(let path): 57 | return .render(view: "404", context: ["path": path], status: .NotFound) 58 | case .RouteNotFound(let path): 59 | return .render(view: "404", context: ["path": path], status: .NotFound) 60 | default: 61 | return nil 62 | } 63 | } 64 | 65 | /// StaticAction is just a predefined configurable handler for serving static files. 66 | /// It's important to pass exactly the same param name to it from the url pattern. 67 | app.get(path: "/assets/:file+", action: StaticAction(path: "public", param:"file")) 68 | 69 | app.get(path: "/hello") { request in 70 | .ok(AnyContent(str: "Hello Express!!!", contentType: "text/html")) 71 | } 72 | 73 | //user as an url param 74 | app.get(path: "/hello/:user.html") { (request) -> Action in 75 | //get user 76 | let user = request.params["user"] 77 | //if there is a user - create our context. If there is no user, context will remain nil 78 | let context = user.map {["user": $0]} 79 | //render our template named "hello" 80 | return .render(view: "hello", context: context) 81 | } 82 | 83 | app.post(path: "/api/user") { (request) -> Action in 84 | //check if JSON has arrived 85 | guard let json = request.body?.asJSON() else { 86 | return .ok("Invalid request") 87 | } 88 | //check if JSON object has username field 89 | guard let username = json["username"].string else { 90 | return .ok("Invalid request") 91 | } 92 | //compose the response as a simple dictionary 93 | let response = 94 | ["status": "ok", 95 | "description": "User with username '" + username + "' created succesfully"] 96 | 97 | //render disctionary as json (remember the one we've registered above?) 98 | return .render(view: JsonView.name, context: response) 99 | } 100 | 101 | //:param - this is how you define a part of URL you want to receive through request object 102 | app.get(path: "/echo/:param") { request in 103 | //here you get the param from request: request.params["param"] 104 | .ok(request.params["param"]) 105 | } 106 | 107 | func factorial(n: Double) -> Double { 108 | return n == 0 ? 1 : n * factorial(n: n - 1) 109 | } 110 | 111 | func calcFactorial(num:Double) -> Future { 112 | return future { 113 | factorial(n: num) 114 | } 115 | } 116 | 117 | // (request -> Future> in) - this is required to tell swift you want to return a Future 118 | // hopefully inference in swift will get better eventually and just "request in" will be enough 119 | app.get(path: "/factorial/:num(\\d+)") { request -> Future> in 120 | // get the number from the url 121 | let num = request.params["num"].flatMap{Double($0)}.getOrElse(el: 0) 122 | 123 | // get the factorial Future. Returns immediately - non-blocking 124 | let factorial = calcFactorial(num: num) 125 | 126 | //map the result of future to Express Action 127 | return factorial.map(String.init).map {.ok($0)} 128 | } 129 | 130 | func testItems(request:Request) throws -> [String: Any] { 131 | let newItems = request.query.map { (k, v) in 132 | (k, v.first!) 133 | } 134 | let items = ["sky": "blue", "fire": "red", "grass": "green"] ++ newItems 135 | 136 | let viewItems = items.map { (k, v) in 137 | ["name": k, "color": v] 138 | } 139 | 140 | if let reason = request.query["throw"]?.first { 141 | throw NastyError.fatal(reason: reason) 142 | } 143 | 144 | return ["test": "ok", "items": viewItems] 145 | } 146 | 147 | app.get(path: "/render.html") { request in 148 | let items = try testItems(request: request) 149 | return .render(view: "test", context: items) 150 | } 151 | 152 | //TODO: make a list of pages 153 | app.get(path: "/") { request -> Action in 154 | let examples:[Any] = [ 155 | ["title": "Hello Express", "link": "/hello", "id":"hello", "code": "code/hello.stencil"], 156 | ["title": "Echo", "link": "/echo?call=hello", "id":"echo", "code": "code/echo.stencil"], 157 | ["title": "Echo with param", "link": "/echo/hello", "id":"echo-param", "code": "code/echo-param.stencil"], 158 | ["title": "Error recoverable (will redirect to recover page)", "link": "/error", "id":"error", "code": "code/error.stencil"], 159 | ["title": "Error fatal", "link": "/error/thebigbanghappened", "id":"error-fatal", "code": "code/error.stencil"], 160 | ["title": "Custom 404", "link": "/thisfiledoesnotexist", "id":"404", "code": "code/404.stencil"], 161 | ["title": "Hello [username]. You can put your name instead", "link": "/hello/username.html", "id":"hello-username", "code": "code/hello-user.stencil"], 162 | ///api/user - implement JSON post form 163 | ["title": "Asynchronous factorial", "link": "/factorial/100", "id":"factorial", "code": "code/factorial.stencil"], 164 | ["title": "Render", "link": "/render.html?sun=yellow&clouds=lightgray", "id":"render", "code": "code/render.stencil"], 165 | ["title": "Redirect", "link": "/test/redirect", "id":"redirect", "code": "code/redirect.stencil"], 166 | ["title": "Merged query (form url encoded and query string)", "link": "/merged/query?some=param&another=param2", "id":"query", "code": "code/query.stencil"], 167 | ] 168 | 169 | let context:[String: Any] = ["examples": examples] 170 | 171 | return .render(view: "index", context: context) 172 | } 173 | 174 | app.get(path: "/test/redirect") { request in 175 | future { 176 | let to = request.query["to"].flatMap{$0.first}.getOrElse(el: "../render.html") 177 | return .redirect(url: to) 178 | } 179 | } 180 | 181 | app.all(path: "/merged/query") { request in 182 | .render(view: JsonView.name, context: request.mergedQuery()) 183 | } 184 | 185 | app.get(path: "/mustache/:hello") { request in 186 | .render(view: "mustache", context: ["hello": request.params["hello"]]) 187 | } 188 | 189 | app.listen(port: 9999).onSuccess { server in 190 | print("Express was successfully launched on port", server.port) 191 | } 192 | 193 | app.run() 194 | -------------------------------------------------------------------------------- /Demo/public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #e4e4e4; 3 | } 4 | 5 | .logo { 6 | display: block; 7 | margin-left: auto; 8 | margin-right: auto; 9 | height: 256px; 10 | } 11 | 12 | h1 { 13 | text-align: center; 14 | } 15 | 16 | .text-big { 17 | font-size: 24pt; 18 | } 19 | 20 | .text-bold { 21 | font-weight: 700; 22 | } 23 | 24 | .example-link { 25 | display: inline-block; 26 | width: 100%; 27 | } 28 | 29 | .right-triangle { 30 | width: 60px; 31 | } 32 | 33 | .d-table { 34 | display: table; 35 | width: 100%; 36 | } 37 | 38 | .d-table-cell { 39 | display: table-cell; 40 | vertical-align: middle; 41 | } 42 | 43 | .panel-heading { 44 | padding-top: 5px; 45 | padding-bottom: 5px; 46 | } 47 | 48 | .panel-title { 49 | padding-right: 5px; 50 | } -------------------------------------------------------------------------------- /Demo/public/css/xcode.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | XCode style (c) Angel Garcia 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #fff; 12 | color: black; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #006a00; 18 | } 19 | 20 | .hljs-keyword, 21 | .hljs-selector-tag, 22 | .hljs-literal { 23 | color: #aa0d91; 24 | } 25 | 26 | .hljs-name { 27 | color: #008; 28 | } 29 | 30 | .hljs-variable, 31 | .hljs-template-variable { 32 | color: #660; 33 | } 34 | 35 | .hljs-string { 36 | color: #c41a16; 37 | } 38 | 39 | .hljs-regexp, 40 | .hljs-link { 41 | color: #080; 42 | } 43 | 44 | .hljs-title, 45 | .hljs-tag, 46 | .hljs-symbol, 47 | .hljs-bullet, 48 | .hljs-number, 49 | .hljs-meta { 50 | color: #1c00cf; 51 | } 52 | 53 | .hljs-section, 54 | .hljs-class .hljs-title, 55 | .hljs-type, 56 | .hljs-attr, 57 | .hljs-built_in, 58 | .hljs-builtin-name, 59 | .hljs-params { 60 | color: #5c2699; 61 | } 62 | 63 | .hljs-attribute, 64 | .hljs-subst { 65 | color: #000; 66 | } 67 | 68 | .hljs-formula { 69 | background-color: #eee; 70 | font-style: italic; 71 | } 72 | 73 | .hljs-addition { 74 | background-color: #baeeba; 75 | } 76 | 77 | .hljs-deletion { 78 | background-color: #ffc8bd; 79 | } 80 | 81 | .hljs-selector-id, 82 | .hljs-selector-class { 83 | color: #9b703f; 84 | } 85 | 86 | .hljs-doctag, 87 | .hljs-strong { 88 | font-weight: bold; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | -------------------------------------------------------------------------------- /Demo/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/Demo/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Demo/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/Demo/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Demo/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/Demo/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Demo/public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/Demo/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /Demo/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/Demo/public/logo.png -------------------------------------------------------------------------------- /Demo/views/404.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swift Express Demo - Error: 404 4 | {% include "css.stencil" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 404: Page Not Found 14 | 15 | Can't find page with path: {{path}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% include "js.stencil" %} 23 | 24 | -------------------------------------------------------------------------------- /Demo/views/code/404.stencil: -------------------------------------------------------------------------------- 1 | /// Custom page not found error handler 2 | app.errorHandler.register { (e:ExpressError) in 3 | switch e { 4 | case .PageNotFound(let path): 5 | return Action<AnyContent>.render("404", context: ["path": path], status: .NotFound) 6 | case .RouteNotFound(let path): 7 | return Action<AnyContent>.render("404", context: ["path": path], status: .NotFound) 8 | default: 9 | return nil 10 | } 11 | } -------------------------------------------------------------------------------- /Demo/views/code/echo-param.stencil: -------------------------------------------------------------------------------- 1 | //:param - this is how you define a part of URL you want to receive through request object 2 | app.get("/echo/:param") { request in 3 | //here you get the param from request: request.params["param"] 4 | return Action.ok(request.params["param"]) 5 | } -------------------------------------------------------------------------------- /Demo/views/code/echo.stencil: -------------------------------------------------------------------------------- 1 | app.get("/echo") { request in 2 | return Action.ok(request.query["call"]?.first) 3 | } -------------------------------------------------------------------------------- /Demo/views/code/error.stencil: -------------------------------------------------------------------------------- 1 | enum NastyError : ErrorType { 2 | case Recoverable 3 | case Fatal(reason:String) 4 | } 5 | 6 | app.get("/error/recovered") { request in 7 | return Action.render("error-recovered", context: [String:Any]()) 8 | } 9 | 10 | app.get("/error/:fatal?") { request in 11 | guard let fatal = request.params["fatal"] else { 12 | throw NastyError.Recoverable 13 | } 14 | throw NastyError.Fatal(reason: fatal) 15 | } 16 | 17 | app.errorHandler.register { (e:NastyError) in 18 | switch e { 19 | case .Recoverable: 20 | return Action.redirect("/error/recovered") 21 | case .Fatal(let reason): 22 | let content = AnyContent( 23 | str: "Unrecoverable nasty error happened. Reason: " + reason 24 | ) 25 | return Action.response(.InternalServerError, content: content) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Demo/views/code/factorial.stencil: -------------------------------------------------------------------------------- 1 | func factorial(n: Double) -> Double { 2 | return n == 0 ? 1 : n * factorial(n - 1) 3 | } 4 | 5 | func calcFactorial(num:Double) -> Future<Double, AnyError> { 6 | return future { 7 | return factorial(num) 8 | } 9 | } 10 | 11 | // (request -> Future> in) - this is required to tell swift you want to return a Future 12 | // hopefully inference in swift will get better eventually and just "request in" will be enough 13 | app.get("/factorial/:num(\\d+)") { request -> Future<Action<AnyContent>, AnyError> in 14 | // get the number from the url 15 | let num = request.params["num"].flatMap{Double($0)}.getOrElse(0) 16 | 17 | // get the factorial Future. Returns immediately - non-blocking 18 | let factorial = calcFactorial(num) 19 | 20 | //map the result of future to Express Action 21 | let future = factorial.map { fac in 22 | Action.ok(String(fac)) 23 | } 24 | 25 | //return the future 26 | return future 27 | } 28 | -------------------------------------------------------------------------------- /Demo/views/code/hello-user.stencil: -------------------------------------------------------------------------------- 1 | //user as an url param 2 | app.get("/hello/:user.html") { request in 3 | //get user 4 | let user = request.params["user"] 5 | //if there is a user - create our context. If there is no user, context will remain nil 6 | let context = user.map {["user": $0]} 7 | //render our template named "hello" 8 | return Action.render("hello", context: context) 9 | } -------------------------------------------------------------------------------- /Demo/views/code/hello.stencil: -------------------------------------------------------------------------------- 1 | app.get("/hello") { request in 2 | return Action.ok( 3 | AnyContent( 4 | str: "<h1><center>Hello Express!!!</center></h1>", 5 | contentType: "text/html" 6 | ) 7 | ) 8 | } -------------------------------------------------------------------------------- /Demo/views/code/query.stencil: -------------------------------------------------------------------------------- 1 | app.all("/merged/query") { request in 2 | Action.render(JsonView.name, context: request.mergedQuery()) 3 | } -------------------------------------------------------------------------------- /Demo/views/code/redirect.stencil: -------------------------------------------------------------------------------- 1 | app.get("/test/redirect") { request in 2 | return future { 3 | let to = request.query["to"].flatMap{$0.first}.getOrElse("../render.html") 4 | return Action.redirect(to) 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/views/code/render.stencil: -------------------------------------------------------------------------------- 1 | func testItems(request:Request) throws -> [String: Any] { 2 | let newItems = request.query.map { (k, v) in 3 | (k, v.first!) 4 | } 5 | let items = ["sky": "blue", "fire": "red", "grass": "green"] ++ newItems 6 | 7 | let viewItems = items.map { (k, v) in 8 | ["name": k, "color": v] 9 | } 10 | 11 | if let reason = request.query["throw"]?.first { 12 | throw NastyError.Fatal(reason: reason) 13 | } 14 | 15 | return ["test": "ok", "items": viewItems] 16 | } 17 | 18 | app.get("/render.html") { request in 19 | let items = try testItems(request) 20 | return Action.render("test", context: items) 21 | } 22 | -------------------------------------------------------------------------------- /Demo/views/colored.stencil: -------------------------------------------------------------------------------- 1 | Item: {{item.name}} -------------------------------------------------------------------------------- /Demo/views/css.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Demo/views/error-recovered.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swift Express Demo - Revoered Error 4 | {% include "css.stencil" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Redirected from error recovering method 13 | 14 | 15 | 16 | 17 | {% include "js.stencil" %} 18 | 19 | -------------------------------------------------------------------------------- /Demo/views/hello.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swift Express Demo - Hello 4 | {% include "css.stencil" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Hello from Stencil: {{user}} 13 | 14 | 15 | 16 | 17 | {% include "js.stencil" %} 18 | 19 | -------------------------------------------------------------------------------- /Demo/views/index.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swift Express Demo 4 | {% include "css.stencil" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Examples: 14 | 15 | {% for example in examples %} 16 | 17 | 18 | 19 | 20 | 21 | {{example.title}} 22 | 23 | 24 | 25 | Try It! 26 | 27 | 28 | 29 | {% include example.code %} 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% include "js.stencil" %} 41 | 47 | 48 | -------------------------------------------------------------------------------- /Demo/views/js.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Demo/views/mustache.mustache: -------------------------------------------------------------------------------- 1 | Echo {{hello}} 2 | -------------------------------------------------------------------------------- /Demo/views/test.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swift Express Demo - Test Stencil: {{test}} 4 | {% include "css.stencil" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Test Stencil: {{test}} 14 | 15 | {% for item in items %} 16 | {% include "colored.stencil" %} 17 | {% endfor %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% include "js.stencil" %} 25 | 26 | -------------------------------------------------------------------------------- /Express/Action.swift: -------------------------------------------------------------------------------- 1 | //===--- Action.swift -----------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Future 24 | import ExecutionContext 25 | 26 | public protocol AbstractActionType { 27 | } 28 | 29 | public protocol ActionType : AbstractActionType { 30 | associatedtype Content 31 | } 32 | 33 | public protocol FlushableAction : AbstractActionType, FlushableType { 34 | } 35 | 36 | public protocol IntermediateActionType : AbstractActionType { 37 | func nextAction(request:Request) -> Future<(AbstractActionType, Request?)> 38 | } 39 | 40 | public protocol SelfSufficientActionType : AbstractActionType { 41 | func handle(app:Express, routeId:String, request:Request, out:DataConsumerType) -> Future 42 | } 43 | 44 | public class Action : ActionType, AbstractActionType { 45 | public typealias Content = C 46 | } 47 | 48 | class ResponseAction : Action, FlushableAction { 49 | let response:Response 50 | 51 | init(response:Response) { 52 | self.response = response 53 | } 54 | 55 | func flushTo(out: DataConsumerType) -> Future { 56 | return response.flushTo(out: out) 57 | } 58 | } 59 | 60 | extension ResponseAction where C : FlushableContent { 61 | convenience init(response:ResponseType) { 62 | let content = response.content.map { content in 63 | C(content: content) 64 | } 65 | let mappedResponse = Response(status: response.status, content: content, headers: response.headers) 66 | self.init(response: mappedResponse) 67 | } 68 | } 69 | 70 | class RenderAction : Action, IntermediateActionType { 71 | let view:String 72 | let context:Context? 73 | let status:StatusCode 74 | let headers:Dictionary 75 | 76 | init(view:String, context:Context?, status:StatusCode = .Ok, headers:Dictionary = Dictionary()) { 77 | self.view = view 78 | self.context = context 79 | self.status = status 80 | self.headers = headers 81 | } 82 | 83 | private func response(status:StatusCode, content:RC?, headers:Dictionary) -> Response { 84 | return Response(status: status, content: content, headers: headers) 85 | } 86 | 87 | func nextAction(request:Request) -> Future<(AbstractActionType, Request?)> { 88 | return request.app.views.render(view: view, context: context).map { content in 89 | let response = Response(status: self.status, content: content, headers: self.headers) 90 | return (ResponseAction(response: response), nil) 91 | } 92 | } 93 | } 94 | 95 | class ChainAction : Action, SelfSufficientActionType { 96 | let request:Request? 97 | 98 | init(request:Request? = nil) { 99 | self.request = request 100 | } 101 | 102 | func handle(app:Express, routeId:String, request:Request, out:DataConsumerType) -> Future { 103 | let req = self.request.map {$0 as RequestHeadType} ?? request 104 | let body = self.request.flatMap {$0.body.flatMap {$0 as ContentType}} ?? request.body 105 | 106 | let route = app.nextRoute(routeId: routeId, request: request) 107 | return route.map { (r:(RouteType, [String: String]))->Future in 108 | let req = req.withParams(params: r.1, app: app) 109 | let transaction = r.0.factory(req, out) 110 | if let body = body { 111 | if !transaction.tryConsume(content: body) { 112 | if let flushableBody = body as? FlushableContentType { 113 | flushableBody.flushTo(out: transaction) 114 | } else { 115 | print("Can not chain this action") 116 | } 117 | } 118 | } 119 | transaction.selfProcess() 120 | return Future(value: ()) 121 | }.getOrElse { 122 | future(context: immediate) { ()->Void in 123 | throw ExpressError.PageNotFound(path: request.path) 124 | } 125 | } 126 | } 127 | } 128 | 129 | public extension Action { 130 | public class func ok(_ content:Content? = nil, headers:Dictionary = Dictionary()) -> Action { 131 | let response = Response(status: 200, content: content, headers: headers) 132 | return ResponseAction(response: response) 133 | } 134 | 135 | internal class func notFound(filename:String) -> Action { 136 | let response = Response(status: 404, content: AnyContent(str: "404 File Not Found\n\n" + filename), headers: Dictionary()) 137 | return ResponseAction(response: response) 138 | } 139 | 140 | internal class func routeNotFound(path:String) -> Action { 141 | let response = Response(status: 404, content: AnyContent(str: "404 Route Not Found\n\n\tpath: " + path), headers: Dictionary()) 142 | return ResponseAction(response: response) 143 | } 144 | 145 | internal class func internalServerError(description:String) -> Action { 146 | let response = Response(status: 500, content: AnyContent(str: "500 Internal Server Error\n\n" + description), headers: Dictionary()) 147 | return ResponseAction(response: response) 148 | } 149 | 150 | internal class func nilRequest() -> Request? { 151 | return nil 152 | } 153 | 154 | public class func chain(request:Request? = nil) -> Action { 155 | return ChainAction(request: request) 156 | } 157 | 158 | public class func chain() -> Action { 159 | return chain(request: nilRequest()) 160 | } 161 | 162 | public class func render(view:String, context:Context? = nil, status:StatusCode = .Ok, headers:Dictionary = Dictionary()) -> Action { 163 | return RenderAction(view: view, context: context, status: status, headers: headers) 164 | } 165 | 166 | public class func response(response:Response) -> Action { 167 | return ResponseAction(response: response) 168 | } 169 | 170 | public class func response(status:UInt16, content:C? = nil, headers:Dictionary = Dictionary()) -> Action { 171 | return response(response: Response(status: status, content: content, headers: headers)) 172 | } 173 | 174 | public class func response(status:StatusCode, content:C? = nil, headers:Dictionary = Dictionary()) -> Action { 175 | return response(status:status.rawValue, content: content, headers: headers) 176 | } 177 | 178 | public class func status(status:UInt16) -> Action { 179 | return response(status: status) 180 | } 181 | 182 | public class func status(status:StatusCode) -> Action { 183 | return self.status(status: status.rawValue) 184 | } 185 | 186 | public class func redirect(url:String, status:RedirectStatusCode) -> Action { 187 | let headers = ["Location": url] 188 | return response(status: status.rawValue, headers: headers) 189 | } 190 | 191 | public class func redirect(url:String, permanent:Bool = false) -> Action { 192 | let code:RedirectStatusCode = permanent ? .MovedPermanently : .TemporaryRedirect 193 | return redirect(url: url, status: code) 194 | } 195 | 196 | public class func found(url:String) -> Action { 197 | return redirect(url: url, status: .Found) 198 | } 199 | 200 | public class func movedPermanently(url:String) -> Action { 201 | return redirect(url: url, status: .MovedPermanently) 202 | } 203 | 204 | public class func seeOther(url:String) -> Action { 205 | return redirect(url: url, status: .SeeOther) 206 | } 207 | 208 | public class func temporaryRedirect(url:String) -> Action { 209 | return redirect(url: url, status: .TemporaryRedirect) 210 | } 211 | } 212 | 213 | public extension Action where C : AnyContent { 214 | public class func ok(_ str:String?) -> Action { 215 | return Action.ok(AnyContent(str: str)) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Express/AnyContent+FormUrlEncoded.swift: -------------------------------------------------------------------------------- 1 | //===--- AnyContent+FormUrlEncoded.swift ----------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | 24 | public extension AnyContent { 25 | func asFormUrlEncoded() -> Dictionary>? { 26 | return contentType.filter {$0 == "application/x-www-form-urlencoded"}.flatMap { _ in 27 | self.asText() 28 | }.map(evhtp_parse_query) 29 | } 30 | } 31 | 32 | public extension Request where C : AnyContent { 33 | public func mergedQuery() -> Dictionary> { 34 | guard let bodyQuery = body?.asFormUrlEncoded() else { 35 | return query 36 | } 37 | 38 | let urlKeys = Set(query.keys) 39 | let keys = Array(urlKeys.union(bodyQuery.keys)) 40 | 41 | let merged = keys.map { key -> (String, Array) in 42 | let allValues = query[key].map { urlValues in 43 | bodyQuery[key].flatMap { bodyValues in 44 | return urlValues + bodyValues 45 | }.getOrElse { 46 | urlValues 47 | } 48 | }.getOrElse { 49 | bodyQuery[key]! 50 | } 51 | 52 | return (key, allValues) 53 | } 54 | return toMap(array: merged) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Express/AnyContent+JSON.swift: -------------------------------------------------------------------------------- 1 | //===--- AnyContent+JSON.swift --------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import SwiftyJSON 24 | 25 | public extension AnyContent { 26 | func asJSON() -> JSON? { 27 | return contentType.filter {$0 == "application/json"}.flatMap { contentType -> JSON? in 28 | do { 29 | guard let text = self.asText() else { 30 | return nil 31 | } 32 | 33 | let json = JSON.parse(string: text) 34 | return json 35 | } catch let e as SwiftyJSONError { 36 | print(e) 37 | return nil 38 | } catch let e { 39 | print(e) 40 | return nil 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Express/AnyContent+MultipartFormData.swift: -------------------------------------------------------------------------------- 1 | //===--- AnyContent+MultipartFormData.swift -------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | 24 | public extension AnyContent { 25 | func asMultipartFormData() throws -> Any? { 26 | throw ExpressError.NotImplemented(description: "multipart form data parsing is not implemented yet") 27 | } 28 | } -------------------------------------------------------------------------------- /Express/AnyContent+XML.swift: -------------------------------------------------------------------------------- 1 | //===--- AnyContent+XML.swift ---------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | 24 | public extension AnyContent { 25 | func asXML() throws -> Any? { 26 | throw ExpressError.NotImplemented(description: "XML parsing is not implemented yet") 27 | } 28 | } -------------------------------------------------------------------------------- /Express/AnyContent.swift: -------------------------------------------------------------------------------- 1 | //===--- AnyContent.swift -------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Future 24 | import ExecutionContext 25 | 26 | public class AnyContentFactory : AbstractContentFactory { 27 | private var data:Array = [] 28 | 29 | public required init(response:RequestHeadType) { 30 | super.init(response: response) 31 | } 32 | 33 | func resolve() { 34 | promise.trySuccess(value: AnyContent(data: self.data, contentType: head.contentType)!) 35 | } 36 | 37 | public override func consume(data:Array) -> Future { 38 | return future(context: immediate) { 39 | self.data += data 40 | if let length = self.head.contentLength { 41 | if(self.data.count >= length) { 42 | self.resolve() 43 | } 44 | } 45 | } 46 | } 47 | 48 | public override func dataEnd() { 49 | resolve() 50 | } 51 | } 52 | 53 | public class AnyContent : ConstructableContentType, FlushableContentType { 54 | public typealias Factory = AnyContentFactory 55 | let data:Array 56 | public let contentType:String? 57 | 58 | public init?(data:Array?, contentType:String?) { 59 | guard let data = data else { 60 | self.data = [] 61 | self.contentType = nil 62 | return nil 63 | } 64 | self.data = data 65 | self.contentType = contentType 66 | } 67 | 68 | public func flushTo(out: DataConsumerType) -> Future { 69 | return out.consume(data: data) 70 | } 71 | } 72 | 73 | // textual extensions 74 | public extension AnyContent { 75 | public convenience init?(str:String?, contentType:String? = nil) { 76 | guard let str = str else { 77 | return nil 78 | } 79 | self.init(data: Array(str.utf8), contentType: contentType) 80 | } 81 | 82 | func asText() -> String? { 83 | return String(bytes: data, encoding: String.Encoding.utf8) 84 | } 85 | } 86 | 87 | // raw data extensions 88 | public extension AnyContent { 89 | func asRaw() -> [UInt8]? { 90 | return data 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Express/CacheControl.swift: -------------------------------------------------------------------------------- 1 | //===--- CacheControl.swift -------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | 24 | public protocol HeaderType { 25 | var key:String {get} 26 | var value:String {get} 27 | 28 | static var key:String {get} 29 | } 30 | 31 | public protocol StringType : Hashable { 32 | static func fromString(string:String) -> Self 33 | } 34 | 35 | extension String : StringType { 36 | public static func fromString(string:String) -> String { 37 | return string 38 | } 39 | } 40 | 41 | public extension Dictionary where Key : StringType, Value : StringType { 42 | public mutating func updateWithHeader(header:HeaderType) { 43 | self.updateValue(Value.fromString(string: header.value), forKey: Key.fromString(string: header.key)) 44 | } 45 | } 46 | 47 | public enum CacheControl { 48 | case NoStore 49 | case NoCache 50 | case Private(maxAge:UInt) 51 | case Public(maxAge:UInt) 52 | } 53 | 54 | extension CacheControl : HeaderType { 55 | public static let key:String = "Cache-Control" 56 | 57 | public var key:String { 58 | get { 59 | return CacheControl.key 60 | } 61 | } 62 | 63 | public var value:String { 64 | get { 65 | switch self { 66 | case .NoStore: return "no-store" 67 | case .NoCache: return "no-cache" 68 | case .Public(let maxAge): 69 | return "max-age=" + String(maxAge) 70 | case .Private(let maxAge): 71 | return "private, max-age=" + String(maxAge) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Express/Content.swift: -------------------------------------------------------------------------------- 1 | //===--- Content.swift ----------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Future 24 | import ExecutionContext 25 | 26 | public protocol ContentFactoryType : DataConsumerType { 27 | associatedtype Content 28 | 29 | init(response:RequestHeadType) 30 | 31 | func tryConsume(content:ContentType) -> Bool 32 | func content() -> Future 33 | } 34 | 35 | public protocol ContentType { 36 | var contentType:String? {get} 37 | } 38 | 39 | public protocol ConstructableContentType : ContentType { 40 | associatedtype Factory: ContentFactoryType 41 | } 42 | 43 | public class ContentFactoryBase { 44 | public let head:RequestHeadType 45 | 46 | public required init(response:RequestHeadType) { 47 | head = response 48 | } 49 | } 50 | 51 | public protocol FlushableContentType : ContentType, FlushableType { 52 | } 53 | 54 | public class FlushableContent : FlushableContentType { 55 | let content:FlushableContentType 56 | public var contentType:String? { 57 | get { 58 | return content.contentType 59 | } 60 | } 61 | 62 | public required init(content:FlushableContentType) { 63 | self.content = content 64 | } 65 | 66 | public func flushTo(out:DataConsumerType) -> Future { 67 | return content.flushTo(out: out) 68 | } 69 | } 70 | 71 | public class AbstractContentFactory : ContentFactoryBase, ContentFactoryType { 72 | public typealias Content = T 73 | var promise:Promise 74 | 75 | public required init(response:RequestHeadType) { 76 | promise = Promise() 77 | super.init(response: response) 78 | } 79 | 80 | public func consume(data:Array) -> Future { 81 | return future(context: immediate) { 82 | throw ExpressError.NotImplemented(description: "Not implemented consume in " + Mirror(reflecting: self).description) 83 | } 84 | } 85 | 86 | public func dataEnd() throws { 87 | throw ExpressError.NotImplemented(description: "Not implemented consume in " + Mirror(reflecting: self).description) 88 | } 89 | 90 | public func tryConsume(content: ContentType) -> Bool { 91 | switch content { 92 | case let match as Content: 93 | //TODO: check this? return value? 94 | promise.trySuccess(value: match) 95 | return true 96 | default: 97 | return false 98 | } 99 | } 100 | 101 | public func content() -> Future { 102 | return promise.future 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Express/EVHTP.swift: -------------------------------------------------------------------------------- 1 | //===--- EVHTP.swift ------------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import CEVHTP 24 | import Result 25 | import ExecutionContext 26 | import Future 27 | import CEvent 28 | 29 | #if os(Linux) 30 | import Glibc 31 | #endif 32 | 33 | internal typealias EVHTPp = UnsafeMutablePointer? 34 | internal typealias EVHTPRequest = UnsafeMutablePointer 35 | internal typealias EVHTPRouteCallback = (EVHTPRequest) -> () 36 | internal typealias EVHTPCallback = UnsafeMutablePointer 37 | 38 | class EVHTPBuffer { 39 | let cbuf: OpaquePointer 40 | let free: Bool 41 | let wnCb: ((EVHTPBuffer, Array) -> ())? 42 | init() { 43 | cbuf = evbuffer_new() 44 | free = true 45 | wnCb = nil 46 | } 47 | init(buf: OpaquePointer) { 48 | cbuf = buf 49 | free = false 50 | wnCb = nil 51 | } 52 | init(writeNotification:@escaping (EVHTPBuffer, Array) -> ()) { 53 | cbuf = evbuffer_new() 54 | free = true 55 | wnCb = writeNotification 56 | } 57 | init(buf: OpaquePointer, writeNotification: @escaping (EVHTPBuffer, Array) -> ()) { 58 | cbuf = buf 59 | wnCb = writeNotification 60 | free = false 61 | } 62 | deinit { 63 | if free { 64 | evbuffer_free(cbuf) 65 | } 66 | } 67 | func write(data: Array) -> Int32 { 68 | let result = evbuffer_add(cbuf, data, data.count) 69 | if result == 0 && wnCb != nil { 70 | wnCb!(self, data) 71 | } 72 | return result 73 | } 74 | func read(bytes: Int) -> Array { 75 | let mbuf:UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: bytes) 76 | let readed = evbuffer_remove(cbuf, mbuf, bytes) 77 | let arr = Array(UnsafeBufferPointer(start: mbuf, count: Int(readed))) 78 | mbuf.deinitialize() 79 | mbuf.deallocate(capacity: bytes) 80 | return arr 81 | } 82 | func length() -> Int { 83 | return evbuffer_get_length(cbuf) 84 | } 85 | } 86 | 87 | private class HeadersDict { 88 | var dict: Dictionary 89 | 90 | init() { 91 | dict = Dictionary() 92 | } 93 | init(dict: Dictionary) { 94 | self.dict = dict 95 | } 96 | 97 | func writeHeaders(headers: UnsafeMutablePointer) { 98 | for (k,v) in dict { 99 | let kvheader = evhtp_kv_new(k, v, 1, 1) 100 | evhtp_kvs_add_kv(headers, kvheader) 101 | } 102 | } 103 | 104 | static func fromHeaders(headers: UnsafeMutablePointer) -> HeadersDict { 105 | var headData = HeadersDict() 106 | 107 | evhtp_kvs_for_each(headers, { (kv, arg) -> Int32 in 108 | 109 | if let arg = UnsafeMutablePointer(OpaquePointer(arg)), let kv = kv { 110 | arg.pointee.addHeader(key: kv.pointee.key, klen: kv.pointee.klen, val: kv.pointee.val, vlen: kv.pointee.vlen) 111 | } 112 | return 0 113 | }, &headData) 114 | 115 | return headData 116 | } 117 | 118 | private func addHeader(key: UnsafeMutablePointer, klen: Int, val: UnsafeMutablePointer, vlen: Int) { 119 | let k = String(bytesNoCopy: key, length: klen, encoding: String.Encoding.utf8, freeWhenDone: false) 120 | let v = String(bytesNoCopy: val, length: vlen, encoding: String.Encoding.utf8, freeWhenDone: false) 121 | dict[k!] = v 122 | } 123 | } 124 | 125 | private class RepeatingHeaderDict { 126 | var dict: Dictionary> 127 | init() { 128 | dict = Dictionary>() 129 | } 130 | init(dict: Dictionary>) { 131 | self.dict = dict 132 | } 133 | 134 | func writeHeaders(headers: UnsafeMutablePointer) { 135 | for (k,vs) in dict { 136 | for v in vs { 137 | let kvheader = evhtp_kv_new(k, v, 1, 1) 138 | evhtp_kvs_add_kv(headers, kvheader) 139 | } 140 | } 141 | } 142 | 143 | static func fromHeaders(headers: UnsafeMutablePointer?) -> RepeatingHeaderDict { 144 | var headData = RepeatingHeaderDict() 145 | 146 | evhtp_kvs_for_each(headers, { (kv, arg) -> (Int32) in 147 | 148 | if let arg = UnsafeMutablePointer(OpaquePointer(arg)), let kv = kv { 149 | arg.pointee.addHeader(key: kv.pointee.key, klen: kv.pointee.klen, val: kv.pointee.val, vlen: kv.pointee.vlen) 150 | } 151 | return 0 152 | }, &headData) 153 | return headData 154 | } 155 | 156 | 157 | 158 | private func addHeader(key: UnsafeMutablePointer, klen: Int, val: UnsafeMutablePointer, vlen: Int) { 159 | var unk:UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: klen) 160 | var unv:UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: vlen) 161 | 162 | var k:String? 163 | var v:String? 164 | 165 | key.withMemoryRebound(to: UInt8.self, capacity: klen){key in 166 | if evhtp_unescape_string(&unk, key , klen) == 0 { 167 | unk!.withMemoryRebound(to: Int8.self, capacity: klen) {iunk in 168 | k = String(bytesNoCopy: unk!, length: strnlen(iunk, klen), encoding: String.Encoding.utf8, freeWhenDone: false) 169 | } 170 | } else { 171 | k = String(bytesNoCopy: key, length: klen, encoding: String.Encoding.utf8, freeWhenDone: false) 172 | } 173 | } 174 | 175 | 176 | val.withMemoryRebound(to: UInt8.self, capacity: vlen){val in 177 | 178 | if evhtp_unescape_string(&unv, val , vlen) == 0 { 179 | unv!.withMemoryRebound(to: Int8.self, capacity: vlen){iunv in 180 | v = String(bytesNoCopy: unv!, length: strnlen(iunv, vlen), encoding: String.Encoding.utf8, freeWhenDone: false) 181 | } 182 | } else { 183 | v = String(bytesNoCopy: val, length: vlen, encoding: String.Encoding.utf8, freeWhenDone: false) 184 | } 185 | } 186 | 187 | 188 | if var a = dict[k!] { 189 | a.append(v!) 190 | } else { 191 | dict[k!] = [v!] 192 | } 193 | unk!.deinitialize() 194 | unk!.deallocate(capacity: klen) 195 | unv!.deinitialize() 196 | unv!.deallocate(capacity: vlen) 197 | } 198 | } 199 | 200 | private class DataReadParams { 201 | let end: Promise 202 | let consumer: DataConsumerType 203 | init(consumer: DataConsumerType, end: Promise) { 204 | self.consumer = consumer 205 | self.end = end 206 | } 207 | } 208 | 209 | private func request_callback(req: EVHTPRequest?, callbk: UnsafeMutableRawPointer?) { 210 | let callback = UnsafeMutablePointer(OpaquePointer(callbk)!).pointee 211 | callback(req!) 212 | evhtp_request_pause(req) 213 | } 214 | 215 | private func sockaddr_size(saddr: UnsafeMutablePointer) -> Int { 216 | #if os(Linux) 217 | switch Int32(saddr.memory.sa_family) { 218 | case AF_INET: 219 | return strideof(sockaddr_in) 220 | case AF_INET6: 221 | return strideof(sockaddr_in6) 222 | case AF_LOCAL: 223 | return strideof(sockaddr_un) 224 | default: 225 | return 0 226 | } 227 | #else 228 | return Int(saddr.pointee.sa_len) 229 | #endif 230 | } 231 | 232 | internal class EVHTPRequestInfo { 233 | private let req:EVHTPRequest 234 | 235 | 236 | 237 | var headers: Dictionary { 238 | get { 239 | return HeadersDict.fromHeaders(headers: req.pointee.headers_in).dict 240 | } 241 | } 242 | 243 | var method: String { 244 | get { 245 | switch evhtp_request_get_method(req) { 246 | case htp_method_GET: 247 | return "GET" 248 | case htp_method_HEAD: 249 | return "HEAD" 250 | case htp_method_POST: 251 | return "POST" 252 | case htp_method_PUT: 253 | return "PUT" 254 | case htp_method_DELETE: 255 | return "DELETE" 256 | case htp_method_MKCOL: 257 | return "MKCOL" 258 | case htp_method_COPY: 259 | return "COPY" 260 | case htp_method_MOVE: 261 | return "MOVE" 262 | case htp_method_OPTIONS: 263 | return "OPTIONS" 264 | case htp_method_PROPFIND: 265 | return "PROPFIND" 266 | case htp_method_PROPPATCH: 267 | return "PROPPATCH" 268 | case htp_method_LOCK: 269 | return "LOCK" 270 | case htp_method_UNLOCK: 271 | return "UNLOCK" 272 | case htp_method_TRACE: 273 | return "TRACE" 274 | case htp_method_CONNECT: 275 | return "CONNECT" 276 | case htp_method_PATCH: 277 | return "PATCH" 278 | default: 279 | return "UNKNOWN" 280 | } 281 | } 282 | } 283 | 284 | var version: String { 285 | get { 286 | switch req.pointee.proto { 287 | case EVHTP_PROTO_10: 288 | return "1.0" 289 | case EVHTP_PROTO_11: 290 | return "1.1" 291 | default: 292 | return "INVALID" 293 | } 294 | } 295 | } 296 | var path: String { 297 | get { 298 | let p = String(validatingUTF8: req.pointee.uri.pointee.path.pointee.full) 299 | if p != nil { 300 | return p! 301 | } 302 | return "" 303 | } 304 | } 305 | var uri: String { 306 | get { 307 | var p = path 308 | let uri = req.pointee.uri.pointee 309 | 310 | let q = UnsafePointer(OpaquePointer(uri.query_raw)).flatMap(String.init(validatingUTF8:)) 311 | if q != nil && q != "" { 312 | p = p + "?" + q! 313 | } 314 | let f = UnsafePointer(OpaquePointer(uri.fragment)).flatMap(String.init(validatingUTF8:)) 315 | 316 | if f != nil && f != "" { 317 | p = p + "#" + f! 318 | } 319 | return p 320 | } 321 | } 322 | var host: String { 323 | get { 324 | var h = String(validatingUTF8: req.pointee.uri.pointee.authority.pointee.hostname) 325 | if h != nil { 326 | let p = req.pointee.uri.pointee.authority.pointee.port 327 | if p > 0 { 328 | h = h! + ":" + String(p) 329 | } 330 | return h! 331 | } 332 | return "" 333 | } 334 | } 335 | var username: String? { 336 | get { 337 | return String(validatingUTF8: req.pointee.uri.pointee.authority.pointee.username) 338 | } 339 | } 340 | var password: String? { 341 | get { 342 | return String(validatingUTF8: req.pointee.uri.pointee.authority.pointee.password) 343 | } 344 | } 345 | var scheme: String { 346 | get { 347 | switch req.pointee.uri.pointee.scheme { 348 | case htp_scheme_none: 349 | return "NONE" 350 | case htp_scheme_ftp: 351 | return "FTP" 352 | case htp_scheme_http: 353 | return "HTTP" 354 | case htp_scheme_https: 355 | return "HTTPS" 356 | case htp_scheme_nfs: 357 | return "NFS" 358 | default: 359 | return "UNKNOWN" 360 | } 361 | } 362 | } 363 | var remoteIp: String { 364 | get { 365 | let mbuf = UnsafeMutablePointer.allocate(capacity: Int(INET6_ADDRSTRLEN)) 366 | let err = getnameinfo(req.pointee.conn.pointee.saddr, UInt32(sockaddr_size(saddr: req.pointee.conn.pointee.saddr)), mbuf, UInt32(INET6_ADDRSTRLEN), nil, 0, NI_NUMERICHOST) 367 | 368 | 369 | 370 | var res = "" 371 | if err == 0 { 372 | let t = String(validatingUTF8: mbuf) 373 | if t != nil { 374 | res = t! 375 | } 376 | } 377 | mbuf.deinitialize() 378 | mbuf.deallocate(capacity: Int(INET6_ADDRSTRLEN)) 379 | return res 380 | } 381 | } 382 | var query: Dictionary> { 383 | get { 384 | return RepeatingHeaderDict.fromHeaders(headers: req.pointee.uri.pointee.query).dict 385 | } 386 | } 387 | 388 | init(req: EVHTPRequest) { 389 | self.req = req 390 | } 391 | } 392 | 393 | internal class _evhtp { 394 | // Can't use EV_TIMEOUT from libevent, some name intersection with OS X module. 395 | let EV_TIMEOUT:Int16 = 1 396 | 397 | init() { 398 | evthread_use_pthreads() 399 | } 400 | 401 | func create_base() -> OpaquePointer { 402 | return event_base_new() 403 | } 404 | 405 | func create_htp(base: OpaquePointer) -> EVHTPp { 406 | let htp = evhtp_new(base, nil) 407 | evhtp_set_parser_flags(htp, EVHTP_PARSE_QUERY_FLAG_IGNORE_HEX | EVHTP_PARSE_QUERY_FLAG_ALLOW_EMPTY_VALS | EVHTP_PARSE_QUERY_FLAG_ALLOW_NULL_VALS | EVHTP_PARSE_QUERY_FLAG_TREAT_SEMICOLON_AS_SEP) 408 | return htp 409 | } 410 | 411 | func bind_address(htp: EVHTPp, host: String, port: UInt16) -> Int32 { 412 | return evhtp_bind_socket(htp, host, port, 1024) 413 | } 414 | 415 | func start_server_loop(base: OpaquePointer) { 416 | event_base_dispatch(base) 417 | } 418 | 419 | func start_event(base: OpaquePointer) -> Future { 420 | let p = Promise() 421 | 422 | event_base_once(base, -1, EV_TIMEOUT, { (fd: Int32, what: Int16, arg: UnsafeMutableRawPointer?) in 423 | try! Unmanaged>.fromOpaque(arg!).takeRetainedValue().success(value: ()) 424 | }, UnsafeMutableRawPointer(Unmanaged.passRetained(p).toOpaque()), nil) 425 | 426 | return p.future 427 | } 428 | 429 | func add_simple_route(htp: EVHTPp, path: String, cb: @escaping EVHTPRouteCallback) -> EVHTPCallback { 430 | let cbp = UnsafeMutablePointer.allocate(capacity: 1) 431 | cbp.initialize(to: cb) 432 | return evhtp_set_cb(htp, path, request_callback, UnsafeMutableRawPointer(cbp)) 433 | } 434 | 435 | func add_general_route(htp: EVHTPp, cb: @escaping EVHTPRouteCallback) { 436 | let cbp = UnsafeMutablePointer.allocate(capacity: 1) 437 | cbp.initialize(to: cb) 438 | evhtp_set_gencb(htp, request_callback, UnsafeMutableRawPointer(cbp)) 439 | } 440 | 441 | func add_wildcard_route(htp: EVHTPp, wpath:String, cb: @escaping EVHTPRouteCallback) -> EVHTPCallback { 442 | let cbp = UnsafeMutablePointer.allocate(capacity: 1) 443 | cbp.initialize(to: cb) 444 | return evhtp_set_glob_cb(htp, wpath, request_callback, cbp) 445 | } 446 | 447 | func get_request_info(req: EVHTPRequest) -> EVHTPRequestInfo { 448 | return EVHTPRequestInfo(req: req) 449 | } 450 | 451 | func read_data(req: EVHTPRequest, cb: (Array) -> Bool) { 452 | let buf = EVHTPBuffer(buf: req.pointee.buffer_in) 453 | var readed = 0 454 | repeat { 455 | let data = buf.read(bytes: 4096) 456 | readed = data.count 457 | if !cb(data) { 458 | break; 459 | } 460 | } while (readed > 0) 461 | } 462 | 463 | func start_response(req: EVHTPRequest, headers:Dictionary, status: UInt16) -> EVHTPBuffer { 464 | HeadersDict(dict: headers).writeHeaders(headers: req.pointee.headers_out) 465 | evhtp_send_reply_chunk_start(req, status) 466 | return EVHTPBuffer(writeNotification: { (buf: EVHTPBuffer, data: Array) -> () in 467 | evhtp_send_reply_chunk(req, buf.cbuf) 468 | }) 469 | } 470 | 471 | func finish_response(req: EVHTPRequest, buffer: EVHTPBuffer) { 472 | evhtp_send_reply_chunk_end(req) 473 | event_base_once(req.pointee.htp.pointee.evbase, -1, EV_TIMEOUT, { (fd: Int32, what: Int16, arg: UnsafeMutableRawPointer?) -> Void in 474 | evhtp_request_resume(EVHTPRequest(OpaquePointer(arg)!)) 475 | }, req, nil) 476 | } 477 | } 478 | 479 | internal let EVHTP = _evhtp() 480 | 481 | func evhtp_parse_query(query:String) -> [String: [String]] { 482 | let parsed = evhtp_parse_query_wflags(query, query.utf8.count,EVHTP_PARSE_QUERY_FLAG_IGNORE_HEX | EVHTP_PARSE_QUERY_FLAG_ALLOW_EMPTY_VALS | EVHTP_PARSE_QUERY_FLAG_ALLOW_NULL_VALS | EVHTP_PARSE_QUERY_FLAG_TREAT_SEMICOLON_AS_SEP) 483 | defer { 484 | evhtp_kvs_free(parsed) 485 | } 486 | return RepeatingHeaderDict.fromHeaders(headers: parsed!).dict 487 | } 488 | -------------------------------------------------------------------------------- /Express/ErrorHandler.swift: -------------------------------------------------------------------------------- 1 | //===--- ErrorHandler.swift -----------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Future 24 | 25 | public protocol ErrorHandlerType { 26 | func handle(e:Error) -> AbstractActionType? 27 | } 28 | 29 | public typealias ErrorHandlerFunction = (Error) -> AbstractActionType? 30 | 31 | class DefaultErrorHandler : ErrorHandlerType { 32 | func handle(e:Error) -> AbstractActionType? { 33 | let errorName = Mirror(reflecting: e).description 34 | let description = "Internal Server Error\n\n" + errorName 35 | return Action.internalServerError(description: description) 36 | } 37 | } 38 | 39 | class FunctionErrorHandler : ErrorHandlerType { 40 | let fun:ErrorHandlerFunction 41 | 42 | init(fun:@escaping ErrorHandlerFunction) { 43 | self.fun = fun 44 | } 45 | 46 | convenience init(fun: @escaping (E) -> AbstractActionType?) { 47 | self.init { e -> AbstractActionType? in 48 | guard let e = e as? E else { 49 | return nil 50 | } 51 | return fun(e) 52 | } 53 | } 54 | 55 | func handle(e:Error) -> AbstractActionType? { 56 | return fun(e) 57 | } 58 | } 59 | 60 | internal let defaultErrorHandler = DefaultErrorHandler() 61 | 62 | public class AggregateErrorHandler : ErrorHandlerType { 63 | internal var handlers:Array = [] 64 | 65 | init() { 66 | register { e in 67 | //this is the only way to check. Otherwise it will just always tall-free bridge to NSError 68 | if type(of: e) == NSError.self { 69 | //stupid autobridging 70 | switch e { 71 | case let e as NSError: 72 | return Action.internalServerError(description: e.description) 73 | default: return nil 74 | } 75 | } else { 76 | return nil 77 | } 78 | } 79 | register(ExpressErrorHandler) 80 | } 81 | 82 | public func register(handler: ErrorHandlerType) { 83 | handlers.insert(handler, at: 0) 84 | } 85 | 86 | public func handle(e:Error) -> AbstractActionType? { 87 | for handler in handlers { 88 | if let action = handler.handle(e: e) { 89 | return action 90 | } 91 | } 92 | return defaultErrorHandler.handle(e: e) 93 | } 94 | } 95 | 96 | //API sugar 97 | 98 | public extension AggregateErrorHandler { 99 | public func register(_ f: @escaping ErrorHandlerFunction) { 100 | register(handler: FunctionErrorHandler(fun: f)) 101 | } 102 | 103 | public func register(_ f: @escaping (Error) -> Action?) { 104 | register(handler: FunctionErrorHandler(fun: f)) 105 | } 106 | 107 | public func register(_ f: @escaping (Error) -> Action?) { 108 | register(handler: FunctionErrorHandler(fun: f)) 109 | } 110 | 111 | public func register(_ f:@escaping (E) -> AbstractActionType?) { 112 | register(handler: FunctionErrorHandler(fun: f)) 113 | } 114 | 115 | public func register(_ f:@escaping (E) -> Action?) { 116 | register(handler: FunctionErrorHandler(fun: f)) 117 | } 118 | 119 | public func register(_ f:@escaping (E) -> Action?) { 120 | register(handler: FunctionErrorHandler(fun: f)) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Express/Errors.swift: -------------------------------------------------------------------------------- 1 | //===--- Errors.swift -----------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | 24 | public protocol ExpressErrorType : Error { 25 | } 26 | 27 | public enum ExpressError : Error { 28 | case NotImplemented(description:String) 29 | case FileNotFound(filename:String) 30 | case PageNotFound(path:String) 31 | case RouteNotFound(path:String) 32 | case NoSuchView(name:String) 33 | case Render(description:String, line:Int?, cause:Error?) 34 | } 35 | 36 | func ExpressErrorHandler(e:Error) -> AbstractActionType? { 37 | guard let e = e as? ExpressError else { 38 | return nil 39 | } 40 | 41 | switch e { 42 | case .NotImplemented(let description): return Action.internalServerError(description: description) 43 | case .FileNotFound(let filename): return Action.internalServerError(description: "File not found: " + filename) 44 | case .NoSuchView(let name): return Action.internalServerError(description: "View not found: " + name) 45 | case .PageNotFound(let path): return Action.notFound(filename: path) 46 | case .RouteNotFound(let path): return Action.routeNotFound(path: path) 47 | case .Render(var description, line: let line, cause: let e): 48 | description += "\n\n" 49 | if (line != nil) { 50 | description.append("At line:" + line!.description + "\n\n") 51 | } 52 | if (e != nil) { 53 | description.append("With error: " + e.debugDescription) 54 | } 55 | return Action.internalServerError(description: "View not found: " + description) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Express/EventExecutionContext.swift: -------------------------------------------------------------------------------- 1 | //===--- EventExecutionContext.swift -------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Boilerplate 23 | import ExecutionContext 24 | import Future 25 | 26 | import CEvent 27 | 28 | //TODO: move to boilerplate and make ALL-functional 29 | class SafeFunction { 30 | let f:SafeTask 31 | 32 | init(_ f:@escaping SafeTask) { 33 | self.f = f 34 | } 35 | } 36 | 37 | extension Timeout { 38 | static let USEC_PER_SEC:Double = 1000*1000 39 | 40 | var sec:Double { 41 | switch self { 42 | case .Infinity: return Double.infinity 43 | case .Immediate: return 0 44 | case .In(timeout: let timeout): return timeout.nextDown 45 | } 46 | } 47 | 48 | var secUsec:(Int, Int) { 49 | switch self { 50 | case .Infinity: return (Int.max, Int.max) 51 | case .Immediate: return (0, 0) 52 | case .In(timeout: let timeout): 53 | let sec = timeout.nextDown 54 | let usec = (timeout-sec) * Timeout.USEC_PER_SEC 55 | return (Int(sec), Int(usec)) 56 | } 57 | } 58 | } 59 | 60 | extension Timeout { 61 | var timeval:timeval? { 62 | switch self { 63 | case .Immediate: return nil 64 | default: 65 | let secUsec = self.secUsec 66 | return CEvent.timeval(tv_sec: Int(secUsec.0), tv_usec: Int32(secUsec.1)) 67 | } 68 | } 69 | } 70 | 71 | func event_base_task(base:OpaquePointer, timeout:UnsafePointer?, task:@escaping SafeTask) { 72 | let function = SafeFunction(task) 73 | 74 | event_base_once(base, -1, Int16(EV_TIMEOUT), { (fd: Int32, what: Int16, arg: UnsafeMutableRawPointer?) in 75 | let task = Unmanaged.fromOpaque(arg!).takeRetainedValue() 76 | task.f() 77 | }, UnsafeMutableRawPointer(Unmanaged.passRetained(function).toOpaque()), timeout) 78 | } 79 | 80 | func event_base_task(base:OpaquePointer, timeout:Timeout = .Immediate, task:@escaping SafeTask) { 81 | var timeval = timeout.timeval 82 | let tvp = timeval.map { _ in 83 | withUnsafePointer(to: &timeval!) {$0} 84 | } 85 | 86 | event_base_task(base: base, timeout: tvp, task: task) 87 | } 88 | 89 | internal class EventExecutionContext : ExecutionContextBase, ExecutionContextProtocol { 90 | //static initialization 91 | static let _ini:Void = { 92 | evthread_use_pthreads() 93 | }() 94 | 95 | let base:OpaquePointer 96 | //TODO: atomic 97 | private var _running:Bool = false 98 | 99 | override init() { 100 | //static initialization 101 | EventExecutionContext._ini 102 | self.base = event_base_new() 103 | super.init() 104 | _run() 105 | } 106 | 107 | deinit { 108 | event_base_loopexit(base, nil) 109 | } 110 | 111 | private func _run() { 112 | if _running { 113 | return 114 | } 115 | 116 | let base = self.base 117 | 118 | try! Thread.detach { [weak self] in 119 | _currentContext.value = self 120 | 121 | self?._running = true 122 | defer { 123 | self?._running = false 124 | } 125 | 126 | let exit = event_base_loop(base, EVLOOP_NO_EXIT_ON_EMPTY) 127 | 128 | print("done:", exit) 129 | } 130 | 131 | 132 | } 133 | 134 | public func sync(task: @escaping () throws -> ReturnType) rethrows -> ReturnType { 135 | return try syncThroughAsync(task: task) 136 | } 137 | 138 | public func async(after: Timeout, task: @escaping SafeTask) { 139 | event_base_task(base: base, timeout: after, task: task) 140 | } 141 | 142 | public func async(task: @escaping SafeTask) { 143 | event_base_task(base: base, task: task) 144 | } 145 | 146 | } 147 | 148 | extension EventExecutionContext : NonStrictEquatable { 149 | func isEqual(to other:NonStrictEquatable) -> Bool { 150 | guard let other = other as? EventExecutionContext else { 151 | return false 152 | } 153 | return self.base == other.base 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Express/ExecutionContext.swift: -------------------------------------------------------------------------------- 1 | //===--- ExecutionContext.swift -------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import ExecutionContext 23 | 24 | private let cmain:ExecutionContextProtocol = ExecutionContext.main 25 | 26 | extension ExecutionContext { 27 | static let main = cmain 28 | static let user = global 29 | static let network = EventExecutionContext() 30 | 31 | static let action = ExecutionContext(kind: .parallel) 32 | static let render = ExecutionContext(kind: .parallel) 33 | static let view = ExecutionContext(kind: .serial) 34 | 35 | class func run() -> Never { 36 | mainProc() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Express/Express.swift: -------------------------------------------------------------------------------- 1 | //===--- Express.swift ----------------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import ExecutionContext 24 | import Future 25 | 26 | public func express() -> Express { 27 | return Express() 28 | } 29 | 30 | public class Express : RouterType { 31 | var routes:Array = [] 32 | var server:ServerType? 33 | public let views:Views = Views() 34 | public let errorHandler:AggregateErrorHandler = AggregateErrorHandler() 35 | 36 | func routeForId(id:String) -> RouteType? { 37 | //TODO: hash it 38 | return routes.findFirst { route in 39 | route.id == id 40 | } 41 | } 42 | 43 | func handleInternal(matcher:UrlMatcherType, handler:@escaping (Request) -> Future>) -> Void { 44 | 45 | let routeId = NSUUID().uuidString 46 | 47 | let factory:TransactionFactory = { head, out in 48 | return Transaction(app: self, routeId: routeId, head: head, out: out, handler: handler) 49 | } 50 | 51 | let route = Route(id: routeId, matcher: matcher, factory: factory) 52 | 53 | routes.append(route) 54 | } 55 | 56 | func handleInternal(matcher:UrlMatcherType, handler:@escaping (Request) throws -> Action) -> Void { 57 | 58 | handleInternal(matcher: matcher) { request in 59 | //execute synchronous request aside from main queue (on a user queue) 60 | future(context: ExecutionContext.user) { 61 | return try handler(request) 62 | } 63 | } 64 | } 65 | } 66 | 67 | public extension Express { 68 | 69 | //sync 70 | func handle(matcher:UrlMatcherType, handler:@escaping (Request) throws -> Action) -> Void { 71 | handleInternal(matcher: matcher, handler: handler) 72 | } 73 | 74 | //async 75 | func handle(matcher:UrlMatcherType, handler: @escaping (Request) -> Future>) -> Void { 76 | handleInternal(matcher: matcher, handler: handler) 77 | } 78 | 79 | //action 80 | func handle(matcher:UrlMatcherType, action:Action) -> Void { 81 | return handle(matcher: matcher) { (request:Request) -> Action in 82 | return action 83 | } 84 | } 85 | } 86 | 87 | public protocol AppContext { 88 | var app:Express {get} 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Express/ExpressServer.swift: -------------------------------------------------------------------------------- 1 | //===--- ExpressServer.swift ----------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Result 24 | import ExecutionContext 25 | import Future 26 | 27 | public extension Express { 28 | func listen(port:UInt16) -> Future { 29 | let server = HttpServer(app: self, port: port) 30 | return server.start() 31 | } 32 | 33 | func run() -> Never { 34 | #if os(Linux) && !dispatch 35 | print("Note: You have built Express without dispatch support. We have implemented this mode to support Linux developers while Dispatch for Linux is not available out of the box. Consider it to be development mode only and not suitable for production as it might cause occasional hanging and crashes. Still, there is a possibility to build Express with dispatch support (recommended for production use). Follow this link for more info: https://github.com/crossroadlabs/Express") 36 | #endif 37 | ExecutionContext.run() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Express/ExpressSugar.swift: -------------------------------------------------------------------------------- 1 | //===--- ExpressSugar.swift -----------------------------------------------===// 2 | // 3 | //Copyright (c) 2015-2016 Daniel Leping (dileping) 4 | // 5 | //This file is part of Swift Express. 6 | // 7 | //Swift Express is free software: you can redistribute it and/or modify 8 | //it under the terms of the GNU Lesser General Public License as published by 9 | //the Free Software Foundation, either version 3 of the License, or 10 | //(at your option) any later version. 11 | // 12 | //Swift Express is distributed in the hope that it will be useful, 13 | //but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | //GNU Lesser General Public License for more details. 16 | // 17 | //You should have received a copy of the GNU Lesser General Public License 18 | //along with Swift Express. If not, see . 19 | // 20 | //===----------------------------------------------------------------------===// 21 | 22 | import Foundation 23 | import Future 24 | import Regex 25 | 26 | private func defaultUrlMatcher(path:String, method:String = HttpMethod.Any.rawValue) -> UrlMatcherType { 27 | return try! RegexUrlMatcher(method: method, pattern: path) 28 | } 29 | 30 | public extension Express { 31 | //sync 32 | //we need it to avoid recursion 33 | internal func handleInternal(method:String, path:String, handler:@escaping (Request) throws -> Action) -> Void { 34 | self.handle(matcher: defaultUrlMatcher(path: path, method: method), handler: handler) 35 | } 36 | 37 | func handle(method:String, path:String, handler:@escaping (Request) throws -> Action) -> Void { 38 | self.handleInternal(method: method, path: path, handler: handler) 39 | } 40 | 41 | func handle(method:String, regex:Regex, handler:@escaping (Request) throws -> Action) -> Void { 42 | self.handle(matcher: RegexUrlMatcher(method: method, regex: regex), handler: handler) 43 | } 44 | 45 | func all(path:String, handler:@escaping (Request) throws -> Action) -> Void { 46 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 47 | } 48 | 49 | func get(path:String, handler:@escaping (Request) throws -> Action) -> Void { 50 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 51 | } 52 | 53 | func post(path:String, handler:@escaping (Request) throws -> Action) -> Void { 54 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 55 | } 56 | 57 | func put(path:String, handler:@escaping (Request) throws -> Action) -> Void { 58 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 59 | } 60 | 61 | func delete(path:String, handler:@escaping (Request) throws -> Action) -> Void { 62 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 63 | } 64 | 65 | func patch(path:String, handler:@escaping (Request) throws -> Action) -> Void { 66 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 67 | } 68 | 69 | //async 70 | //we need it to avoid recursion 71 | internal func handleInternal(method:String, path:String, handler:@escaping (Request) -> Future>) -> Void { 72 | handleInternal(matcher: defaultUrlMatcher(path: path, method: method), handler: handler) 73 | } 74 | 75 | func handle(method:String, path:String, handler:@escaping (Request) -> Future>) -> Void { 76 | handleInternal(method: method, path: path, handler: handler) 77 | } 78 | 79 | func handle(method:String, regex:Regex, handler:@escaping(Request) -> Future>) -> Void { 80 | self.handle(matcher: RegexUrlMatcher(method: method, regex: regex), handler: handler) 81 | } 82 | 83 | func all(path:String, handler:@escaping (Request) -> Future>) -> Void { 84 | handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 85 | } 86 | 87 | func get(path:String, handler:@escaping (Request) -> Future>) -> Void { 88 | handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 89 | } 90 | 91 | func post(path:String, handler:@escaping (Request) -> Future>) -> Void { 92 | handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 93 | } 94 | 95 | func put(path:String, handler:@escaping (Request) -> Future>) -> Void { 96 | handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 97 | } 98 | 99 | func delete(path:String, handler:@escaping (Request) -> Future>) -> Void { 100 | handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 101 | } 102 | 103 | func patch(path:String, handler:@escaping (Request) -> Future>) -> Void { 104 | handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 105 | } 106 | 107 | //action 108 | func handle(method:String, path:String, action:Action) -> Void { 109 | handle(matcher: defaultUrlMatcher(path: path, method: method), action: action) 110 | } 111 | 112 | func all(path:String, action:Action) -> Void { 113 | handle(method: HttpMethod.Any.rawValue, path: path, action: action) 114 | } 115 | 116 | func get(path:String, action:Action) -> Void { 117 | handle(method: HttpMethod.Get.rawValue, path: path, action: action) 118 | } 119 | 120 | func post(path:String, action:Action) -> Void { 121 | handle(method: HttpMethod.Post.rawValue, path: path, action: action) 122 | } 123 | 124 | func put(path:String, action:Action) -> Void { 125 | handle(method: HttpMethod.Put.rawValue, path: path, action: action) 126 | } 127 | 128 | func delete(path:String, action:Action) -> Void { 129 | handle(method: HttpMethod.Delete.rawValue, path: path, action: action) 130 | } 131 | 132 | func patch(path:String, action:Action) -> Void { 133 | handle(method: HttpMethod.Patch.rawValue, path: path, action: action) 134 | } 135 | 136 | //sync - simple req 137 | func handle(method:String, path:String, handler:@escaping (Request
{% include example.code %}