├── .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 | 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) throws -> Action) -> Void { 138 | self.handleInternal(method: method, path: path, handler: handler) 139 | } 140 | 141 | func all(path:String, handler:@escaping(Request)throws -> Action) -> Void { 142 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 143 | } 144 | 145 | func get(path:String, handler:@escaping(Request) throws -> Action) -> Void { 146 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 147 | } 148 | 149 | func post(path:String, handler:@escaping(Request) throws -> Action) -> Void { 150 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 151 | } 152 | 153 | func put(path:String, handler:@escaping(Request)throws -> Action) -> Void { 154 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 155 | } 156 | 157 | func delete(path:String, handler:@escaping(Request)throws -> Action) -> Void { 158 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 159 | } 160 | 161 | func patch(path:String, handler:@escaping(Request) throws -> Action) -> Void { 162 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 163 | } 164 | 165 | //async - simple req 166 | func handle(method:String, path:String, handler:@escaping (Request) -> Future>) -> Void { 167 | self.handleInternal(method: method, path: path, handler: handler) 168 | } 169 | 170 | func all(path:String, handler:@escaping(Request) -> Future>) -> Void { 171 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 172 | } 173 | 174 | func get(path:String, handler:@escaping(Request) -> Future>) -> Void { 175 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 176 | } 177 | 178 | func post(path:String, handler:@escaping(Request) -> Future>) -> Void { 179 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 180 | } 181 | 182 | func put(path:String, handler:@escaping(Request) -> Future>) -> Void { 183 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 184 | } 185 | 186 | func delete(path:String, handler:@escaping(Request) -> Future>) -> Void { 187 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 188 | } 189 | 190 | func patch(path:String, handler:@escaping(Request) -> Future>) -> Void { 191 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 192 | } 193 | 194 | //sync - simple res 195 | func handle(method:String, path:String, handler:@escaping (Request) throws -> Action) -> Void { 196 | self.handleInternal(method: method, path: path, handler: handler) 197 | } 198 | 199 | func all(path:String, handler:@escaping (Request) throws -> Action) -> Void { 200 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 201 | } 202 | 203 | func get(path:String, handler:@escaping (Request) throws -> Action) -> Void { 204 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 205 | } 206 | 207 | func post(path:String, handler:@escaping (Request) throws -> Action) -> Void { 208 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 209 | } 210 | 211 | func put(path:String, handler:@escaping (Request) throws -> Action) -> Void { 212 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 213 | } 214 | 215 | func delete(path:String, handler:@escaping (Request) throws -> Action) -> Void { 216 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 217 | } 218 | 219 | func patch(path:String, handler:@escaping (Request) throws -> Action) -> Void { 220 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 221 | } 222 | 223 | //async - simple res 224 | func handle(method:String, path:String, handler:@escaping (Request) -> Future>) -> Void { 225 | self.handleInternal(method: method, path: path, handler: handler) 226 | } 227 | 228 | func all(path:String, handler:@escaping (Request) -> Future>) -> Void { 229 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 230 | } 231 | 232 | func get(path:String, handler:@escaping (Request) -> Future>) -> Void { 233 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 234 | } 235 | 236 | func post(path:String, handler:@escaping (Request) -> Future>) -> Void { 237 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 238 | } 239 | 240 | func put(path:String, handler:@escaping (Request) -> Future>) -> Void { 241 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 242 | } 243 | 244 | func delete(path:String, handler:@escaping (Request) -> Future>) -> Void { 245 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 246 | } 247 | 248 | func patch(path:String, handler:@escaping (Request) -> Future>) -> Void { 249 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 250 | } 251 | 252 | //sync - simple all 253 | func handle(method:String, path:String, handler:@escaping (Request) throws -> Action) -> Void { 254 | self.handleInternal(method: method, path: path, handler: handler) 255 | } 256 | 257 | func all(path:String, handler:@escaping(Request) throws -> Action) -> Void { 258 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 259 | } 260 | 261 | func get(path:String, handler:@escaping(Request) throws -> Action) -> Void { 262 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 263 | } 264 | 265 | func post(path:String, handler:@escaping(Request) throws -> Action) -> Void { 266 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 267 | } 268 | 269 | func put(path:String, handler:@escaping(Request) throws -> Action) -> Void { 270 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 271 | } 272 | 273 | func delete(path:String, handler:@escaping(Request) throws -> Action) -> Void { 274 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 275 | } 276 | 277 | func patch(path:String, handler:@escaping(Request) throws -> Action) -> Void { 278 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 279 | } 280 | 281 | //async - simple all 282 | func handle(method:String, path:String, handler:@escaping (Request) -> Future>) -> Void { 283 | self.handleInternal(method: method, path: path, handler: handler) 284 | } 285 | 286 | func all(path:String, handler:@escaping (Request) -> Future>) -> Void { 287 | self.handle(method: HttpMethod.Any.rawValue, path: path, handler: handler) 288 | } 289 | 290 | func get(path:String, handler:@escaping (Request) -> Future>) -> Void { 291 | self.handle(method: HttpMethod.Get.rawValue, path: path, handler: handler) 292 | } 293 | 294 | func post(path:String, handler:@escaping (Request) -> Future>) -> Void { 295 | self.handle(method: HttpMethod.Post.rawValue, path: path, handler: handler) 296 | } 297 | 298 | func put(path:String, handler:@escaping (Request) -> Future>) -> Void { 299 | self.handle(method: HttpMethod.Put.rawValue, path: path, handler: handler) 300 | } 301 | 302 | func delete(path:String, handler:@escaping (Request) -> Future>) -> Void { 303 | self.handle(method: HttpMethod.Delete.rawValue, path: path, handler: handler) 304 | } 305 | 306 | func patch(path:String, handler:@escaping (Request) -> Future>) -> Void { 307 | self.handle(method: HttpMethod.Patch.rawValue, path: path, handler: handler) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /Express/Functional.swift: -------------------------------------------------------------------------------- 1 | //===--- Functional.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 | func toMap(array:Array<(A, B)>) -> Dictionary { 25 | var dict = Dictionary() 26 | for (k, v) in array { 27 | dict[k] = v 28 | } 29 | return dict 30 | } 31 | 32 | extension Sequence { 33 | func fold(initial:B, f:(B, Iterator.Element)->B) -> B { 34 | var b:B = initial 35 | forEach { e in 36 | b = f(b, e) 37 | } 38 | return b 39 | } 40 | 41 | func findFirst(f:(Iterator.Element)->Bool) -> Iterator.Element? { 42 | for e in self { 43 | if(f(e)) { 44 | return e 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func mapFirst(f:(Iterator.Element)->B?) -> B? { 51 | for e in self { 52 | let b = f(e) 53 | if b != nil { 54 | return b 55 | } 56 | } 57 | return nil 58 | } 59 | } 60 | 61 | /*Just an example 62 | 63 | 64 | let arr = [1, 2, 4] 65 | 66 | let folded = arr.fold(1) { b, e in 67 | return b+e 68 | } 69 | 70 | print("B: ", folded)*/ 71 | 72 | public extension Optional { 73 | func getOrElse( el:@autoclosure () -> Wrapped) -> Wrapped { 74 | switch self { 75 | case .some(let value): return value 76 | default: return el() 77 | } 78 | } 79 | 80 | func getOrElse(el:() -> Wrapped) -> Wrapped { 81 | switch self { 82 | case .some(let value): return value 83 | default: return el() 84 | } 85 | } 86 | } 87 | 88 | public extension Dictionary { 89 | mutating func getOrInsert(key:Key, f:@autoclosure ()->Value) -> Value { 90 | return getOrInsert(key: key, f: f) 91 | } 92 | 93 | mutating func getOrInsert(key:Key, f:()->Value) -> Value { 94 | guard let stored = self[key] else { 95 | let value = f() 96 | self[key] = value 97 | return value 98 | } 99 | return stored 100 | } 101 | 102 | mutating func getOrInsert(key:Key, f:@autoclosure () throws ->Value) throws -> Value { 103 | return try getOrInsert(key: key, f: f) 104 | } 105 | 106 | mutating func getOrInsert(key:Key, f:() throws -> Value) throws -> Value { 107 | guard let stored = self[key] else { 108 | let value = try f() 109 | self[key] = value 110 | return value 111 | } 112 | return stored 113 | } 114 | } 115 | 116 | public extension Dictionary { 117 | func map( transform: ((Key, Value)) throws -> (K, V)) rethrows -> Dictionary { 118 | var result = Dictionary() 119 | for it in self { 120 | let (k, v) = try transform(it) 121 | result[k] = v 122 | } 123 | return result 124 | } 125 | } 126 | 127 | infix operator ++ { associativity left precedence 160 } 128 | 129 | public func ++ (left:Dictionary, right:Dictionary) -> Dictionary { 130 | var new = left 131 | for (k, v) in right { 132 | new[k] = v 133 | } 134 | return new 135 | } 136 | -------------------------------------------------------------------------------- /Express/Headers.swift: -------------------------------------------------------------------------------- 1 | //===--- Headers.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 enum HttpHeader : String { 26 | case ContentType = "Content-Type" 27 | case ContentLength = "Content-Length" 28 | 29 | func header(headers:Dictionary) -> String? { 30 | return headers[self.rawValue] 31 | } 32 | 33 | func headerInt(headers:Dictionary) -> Int? { 34 | return header(headers: headers).flatMap { str in Int(str) } 35 | } 36 | } 37 | 38 | public protocol HttpHeadType { 39 | var headers:Dictionary {get} 40 | } 41 | 42 | public class HttpHead : HttpHeadType { 43 | public let headers:Dictionary 44 | 45 | init(head:HttpHeadType) { 46 | headers = head.headers 47 | } 48 | 49 | init(headers:Dictionary) { 50 | self.headers = headers 51 | } 52 | } 53 | 54 | public protocol HttpResponseHeadType : HttpHeadType { 55 | var status:UInt16 {get} 56 | } 57 | 58 | public class HttpResponseHead : HttpHead, HttpResponseHeadType, FlushableType { 59 | public let status:UInt16 60 | 61 | init(status:UInt16, head:HttpHeadType) { 62 | self.status = status 63 | super.init(head: head) 64 | } 65 | 66 | init(status:UInt16, headers:Dictionary) { 67 | self.status = status 68 | super.init(headers: headers) 69 | } 70 | 71 | //all the code below should be moved to Streams+Headers and made as an extension 72 | //unfortunately swift does not allow to override functions introduced in extensions yet 73 | //should be moved as soon as the feature is implemented in swift 74 | public func flushTo(out:DataConsumerType) -> Future { 75 | if let headOut = out as? ResponseHeadDataConsumerType { 76 | return headOut.consume(head: self) 77 | } else { 78 | return out.consume(data: serializeHead()) 79 | } 80 | } 81 | 82 | func serializeHead() -> Array { 83 | //TODO: move to real serializer 84 | var r = "HTTP/1.1 " + status.description + " OK\n" 85 | for header in headers { 86 | r += header.0 + ": " + header.1 + "\n" 87 | } 88 | r += "\n" 89 | return Array(r.utf8) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Express/HttpMethod.swift: -------------------------------------------------------------------------------- 1 | //===--- HttpMethod.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 enum HttpMethod : String { 25 | case `Any` = "*" 26 | case Get = "GET" 27 | case Post = "POST" 28 | case Put = "PUT" 29 | case Delete = "DELETE" 30 | case Patch = "PATCH" 31 | } 32 | -------------------------------------------------------------------------------- /Express/HttpServer.swift: -------------------------------------------------------------------------------- 1 | //===--- HttpServer.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 Future 25 | #if os(Linux) 26 | import Glibc 27 | #endif 28 | import ExecutionContext 29 | 30 | private class ServerParams { 31 | let port: UInt16 32 | let app:Express 33 | 34 | init(port: UInt16, app: Express) { 35 | self.port = port 36 | self.app = app 37 | } 38 | } 39 | 40 | private class ResponseDataConsumer : ResponseHeadDataConsumerType { 41 | let sock:EVHTPRequest 42 | var buffer: EVHTPBuffer? 43 | 44 | init(sock: EVHTPRequest) { 45 | self.sock = sock 46 | self.buffer = nil 47 | } 48 | 49 | func consume(head: HttpResponseHeadType) -> Future { 50 | return future(context: ExecutionContext.network) { 51 | //TODO: handle errors if any 52 | if let h = head as? HttpResponseHead { 53 | self.buffer = EVHTP.start_response(req: self.sock, headers: h.headers, status: h.status) 54 | } else { 55 | self.buffer = EVHTP.start_response(req: self.sock, headers: Dictionary(), status: head.status) 56 | } 57 | } 58 | } 59 | 60 | func consume(data:Array) -> Future { 61 | return future(context: ExecutionContext.network) { 62 | //TODO: handle errors if any 63 | self.buffer?.write(data: data) 64 | } 65 | } 66 | 67 | func dataEnd() throws { 68 | ExecutionContext.network.async { 69 | //TODO: handle errors if any 70 | EVHTP.finish_response(req: self.sock, buffer: self.buffer!) 71 | self.buffer = nil 72 | } 73 | } 74 | } 75 | 76 | private func handle_request(req: EVHTPRequest, serv:ServerParams) { 77 | //TODO: implement request data parsing 78 | 79 | let info = EVHTP.get_request_info(req: req) 80 | let head = RequestHead(app: serv.app, method: info.method, version: info.version, remoteAddress: info.remoteIp, secure: info.scheme == "HTTPS", uri: info.uri, path: info.path, query: info.query, headers: info.headers, params: Dictionary()) 81 | let os = ResponseDataConsumer(sock: req) 82 | 83 | let routeTuple = serv.app.firstRoute(request: head) 84 | let transaction = routeTuple.map { 85 | ($0.0, head.withParams(params: $0.1, app: serv.app)) 86 | }.map { ( route, header) in 87 | route.factory(header, os) 88 | } 89 | 90 | 91 | /* .getOrElse(Transaction(app: serv.app, routeId: "", head: head, out: os)) 92 | 93 | let route = routeTuple.0 94 | let header = head.withParams(routeTuple.1)*/ 95 | 96 | if let transaction = transaction { 97 | transaction.selfProcess() 98 | EVHTP.read_data(req: req, cb: { data in 99 | if data.count > 0 { 100 | //TODO: handle consumption success or error 101 | transaction.consume(data: data) 102 | } else { 103 | //TODO: handle errors (for now silencing it with try!) 104 | try! transaction.dataEnd() 105 | } 106 | return true 107 | }) 108 | } else { 109 | let transaction = Transaction(app: serv.app, routeId: "", head: head, out: os) 110 | let action = future(context: immediate) { () throws -> AbstractActionType in 111 | throw ExpressError.RouteNotFound(path: head.path) 112 | } 113 | transaction.handleAction(action: action, request: Optional>.none) 114 | try! transaction.dataEnd() 115 | } 116 | } 117 | 118 | private func setup_server(params serv:ServerParams) -> Bool { 119 | let base = ExecutionContext.network.base 120 | 121 | let htp_serv = EVHTP.create_htp(base: base) 122 | let bound = EVHTP.bind_address(htp: htp_serv, host: "0.0.0.0", port: serv.port) 123 | EVHTP.add_general_route(htp: htp_serv) { (req: EVHTPRequest) -> () in 124 | handle_request(req: req, serv: serv) 125 | } 126 | return bound == 0 127 | } 128 | 129 | class HttpServer : ServerType { 130 | let port:UInt16 131 | let app:Express 132 | let thread: UnsafeMutablePointer 133 | 134 | func start() -> Future { 135 | let params = ServerParams(port: port, app: app) 136 | return future(context:ExecutionContext.network) { 137 | setup_server(params: params) 138 | }.filter {$0}.map {_ in self} 139 | } 140 | 141 | required init(app:Express, port:UInt16) { 142 | self.port = port 143 | self.app = app 144 | self.thread = UnsafeMutablePointer.allocate(capacity: 1) 145 | } 146 | 147 | deinit { 148 | self.thread.deinitialize() 149 | self.thread.deallocate(capacity: 1) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Express/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015-2016 Daniel Leping (dileping). All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Express/JsonView.swift: -------------------------------------------------------------------------------- 1 | //===--- JsonView.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 protocol JSONConvertible { 26 | func toJSON() -> JSON? 27 | } 28 | 29 | extension Bool : JSONConvertible { 30 | public func toJSON() -> JSON? { 31 | return JSON(self) 32 | } 33 | } 34 | 35 | extension Double : JSONConvertible { 36 | public func toJSON() -> JSON? { 37 | return JSON(self) 38 | } 39 | } 40 | 41 | extension Int : JSONConvertible { 42 | public func toJSON() -> JSON? { 43 | return JSON(self) 44 | } 45 | } 46 | 47 | extension String : JSONConvertible { 48 | public func toJSON() -> JSON? { 49 | return JSON(self) 50 | } 51 | } 52 | 53 | extension Array : JSONConvertible { 54 | public func toJSON() -> JSON? { 55 | return JSON(self.flatMap { $0 as? JSONConvertible }.flatMap {$0.toJSON()}) 56 | } 57 | } 58 | 59 | extension Dictionary : JSONConvertible { 60 | public func toJSON() -> JSON? { 61 | let normalized = self.map {(String(describing: $0), $1)}.flatMap { (k, v) in 62 | (v as? JSONConvertible).map {(k, $0)} 63 | } 64 | return JSON(toMap(array: normalized.flatMap { (k, v) in 65 | v.toJSON().map {(k, $0)} 66 | })) 67 | } 68 | } 69 | 70 | extension Optional { 71 | public func toJSON() -> JSON? { 72 | return self.flatMap{$0 as? JSONConvertible}.flatMap{$0.toJSON()} 73 | } 74 | } 75 | 76 | public class JsonView : NamedViewType { 77 | public static let name:String = "json" 78 | public let name:String = JsonView.name 79 | 80 | public init() { 81 | } 82 | 83 | public func render(context:Context?) throws -> FlushableContentType { 84 | //TODO: implement reflection 85 | let json = context.flatMap{$0 as? JSONConvertible}.flatMap { $0.toJSON() } 86 | 87 | //TODO: avoid string path 88 | guard let render = json?.rawString() else { 89 | throw ExpressError.Render(description: "unable to render json: " + context.flatMap{String(describing: $0)}.getOrElse(el: "None"), line: nil, cause: nil) 90 | } 91 | return AnyContent(str:render, contentType: "application/json")! 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Express/MustacheViewEngine.swift: -------------------------------------------------------------------------------- 1 | //===--- MustacheViewEngine.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 | private let message = "Mustache rendering engine is not supported on Linux (probably yet). Use Stencil engine if you want to run Express on Linux" 25 | private let warning = "Warning: " + message 26 | 27 | #if !os(Linux) 28 | import Mustache 29 | 30 | typealias MustacheEdible = AnyObject 31 | 32 | private protocol MustacheCookable { 33 | func cook() -> MustacheEdible 34 | } 35 | 36 | extension Dictionary : MustacheCookable { 37 | func cook() -> MustacheEdible { 38 | let s = self.map { (k,v) in 39 | (String(describing: k), v as! AnyObject) 40 | } 41 | let dict:NSDictionary = NSDictionary(dictionary: s) 42 | return dict 43 | } 44 | } 45 | 46 | class MustacheView : ViewType { 47 | let template:Template 48 | 49 | init(template:Template) { 50 | self.template = template 51 | } 52 | 53 | func render(context:Context?) throws -> FlushableContentType { 54 | do { 55 | let anyContext = context.flatMap { (i)->AnyObject? in 56 | if let obj = i as? AnyObject { 57 | return obj 58 | } 59 | guard let cookable = i as? MustacheCookable else { 60 | return nil 61 | } 62 | return cookable.cook() 63 | } 64 | let box = Box(anyContext as? MustacheBoxable) 65 | let render = try template.render(with: box) 66 | return AnyContent(str:render, contentType: "text/html")! 67 | } catch let e as MustacheError { 68 | switch e.kind { 69 | //TODO: double check no such error can be found at this place 70 | //case MustacheError.Kind.TemplateNotFound: throw ExpressError.FileNotFound(filename: <#T##String#>) 71 | default: throw ExpressError.Render(description: e.description, line: e.lineNumber, cause: e) 72 | } 73 | } 74 | } 75 | } 76 | 77 | public class MustacheViewEngine : ViewEngineType { 78 | public init() { 79 | print(warning) 80 | } 81 | 82 | public func extensions() -> Array { 83 | return ["mustache"] 84 | } 85 | 86 | public func view(filePath:String) throws -> ViewType { 87 | do { 88 | let template = try Template(path: filePath) 89 | return MustacheView(template: template) 90 | } catch let e as MustacheError { 91 | switch e.kind { 92 | case MustacheError.Kind.TemplateNotFound: throw ExpressError.FileNotFound(filename: filePath) 93 | default: throw ExpressError.Render(description: e.description, line: e.lineNumber, cause: e) 94 | } 95 | } 96 | } 97 | } 98 | #else 99 | public class MustacheViewEngine : ViewEngineType { 100 | public init() { 101 | print(warning) 102 | } 103 | 104 | public func extensions() -> Array { 105 | return ["mustache"] 106 | } 107 | 108 | public func view(filePath:String) throws -> ViewType { 109 | throw ExpressError.NotImplemented(description: message) 110 | } 111 | } 112 | #endif 113 | -------------------------------------------------------------------------------- /Express/RegexUrlMatcher.swift: -------------------------------------------------------------------------------- 1 | //===--- RegexUrlMatcher.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 Regex 23 | import PathToRegex 24 | 25 | public class RegexUrlMatcher : UrlMatcherType { 26 | private let method:String 27 | private let regex:Regex 28 | 29 | public init(method:String, regex:Regex) { 30 | self.method = method 31 | self.regex = regex 32 | } 33 | 34 | public convenience init(method:String, pattern:String) throws { 35 | self.init(method: method, regex: try Regex(path: pattern)) 36 | } 37 | 38 | /// 39 | /// Matches path with a route and returns matched params if avalable. 40 | /// - Parameter path: path to match over 41 | /// - Returns: nil if route does not match. Matched params otherwise 42 | /// 43 | public func match(method:String, path:String) -> [String: String]? { 44 | if self.method != "*" && self.method != method { 45 | return nil 46 | } 47 | guard let found = regex.findFirst(in: path) else { 48 | return nil 49 | } 50 | let valsArray = regex.groupNames.map { name in 51 | (name, found.group(named: name)) 52 | }.filter {$0.1 != nil} . map { tuple in 53 | (tuple.0, tuple.1!) 54 | } 55 | return toMap(array: valsArray) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Express/Request.swift: -------------------------------------------------------------------------------- 1 | //===--- Request.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 RequestHeadersType { 25 | var contentLength:Int? {get} 26 | var contentType:String? {get} 27 | } 28 | 29 | public protocol RequestHeadType : HttpHeadType, RequestHeadersType { 30 | // HTTP method 31 | var method:String {get} 32 | 33 | // HTTP protocol version 34 | var version:String {get} 35 | 36 | // address of the client 37 | var remoteAddress:String {get} 38 | 39 | // if the connection is sequre (HTTPS) 40 | var secure: Bool {get} 41 | 42 | // full URI component from request 43 | var uri:String {get} 44 | 45 | // request path without query string 46 | var path:String {get} 47 | 48 | // parsed query string {get} 49 | var query:Dictionary> {get} 50 | 51 | // name params parsed from the URL pattern 52 | var params:Dictionary {get} 53 | 54 | var headers:Dictionary {get} 55 | 56 | init(app:Express, method:String, version:String, remoteAddress:String, secure: Bool, uri:String, path:String, query:Dictionary>, headers:Dictionary, params:Dictionary) 57 | } 58 | 59 | public class RequestHead : HttpHead, RequestHeadType { 60 | public let method:String 61 | public let version:String 62 | public let remoteAddress:String 63 | public let secure: Bool 64 | public let uri:String 65 | public let path:String 66 | public let query:Dictionary> 67 | public let params:Dictionary 68 | 69 | public let contentLength:Int? 70 | public let contentType:String? 71 | 72 | public init(head:RequestHeadType) { 73 | contentLength = head.contentLength 74 | contentType = head.contentType 75 | remoteAddress = head.remoteAddress 76 | secure = head.secure 77 | uri = head.uri 78 | path = head.path 79 | query = head.query 80 | params = head.params 81 | 82 | method = head.method 83 | version = head.version 84 | super.init(head: head) 85 | } 86 | 87 | public required init(app:Express, method:String, version:String, remoteAddress:String, secure: Bool, uri:String, path:String, query:Dictionary>, headers:Dictionary, params:Dictionary) { 88 | self.method = method 89 | self.version = version 90 | self.remoteAddress = remoteAddress 91 | self.secure = secure 92 | self.uri = uri 93 | self.path = path 94 | self.query = query 95 | self.params = params 96 | 97 | contentLength = HttpHeader.ContentLength.headerInt(headers: headers) 98 | contentType = HttpHeader.ContentType.header(headers: headers) 99 | super.init(headers: headers) 100 | } 101 | } 102 | 103 | public protocol RequestType : RequestHeadType, AppContext { 104 | associatedtype Content 105 | 106 | var body:Content? {get} 107 | } 108 | 109 | public class Request : RequestHead, RequestType { 110 | public let app:Express 111 | public let body:Content? 112 | 113 | public typealias Content = C 114 | 115 | init(app:Express, head: RequestHeadType, body:Content?) { 116 | self.app = app 117 | self.body = body 118 | super.init(head: head) 119 | } 120 | 121 | public required init(app:Express, method:String, version:String, remoteAddress:String, secure: Bool, uri:String, path:String, query:Dictionary>, headers:Dictionary, params:Dictionary) { 122 | self.app = app 123 | self.body = nil 124 | super.init(app: app, method: method, version: version, remoteAddress: remoteAddress, secure: secure, uri: uri, path: path, query: query, headers: headers, params: params) 125 | } 126 | } 127 | 128 | extension RequestHeadType { 129 | func withParams(params:Dictionary, app:Express) -> Self { 130 | return Self(app: app, method: self.method, version: self.version, remoteAddress: self.remoteAddress, secure: self.secure, uri: self.uri, path: self.path, query: self.query, headers: headers, params: params) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Express/Response.swift: -------------------------------------------------------------------------------- 1 | //===--- Response.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 | //TODO: refactor 27 | protocol HeadersAdjuster { 28 | associatedtype Content : FlushableContentType 29 | static func adjustHeaders(headers:Dictionary, c:ContentType?) -> Dictionary 30 | } 31 | 32 | public protocol ResponseType : HttpResponseHeadType { 33 | var content:FlushableContentType? {get} 34 | } 35 | 36 | public class Response : HttpResponseHead, HeadersAdjuster, ResponseType { 37 | typealias Content = C 38 | 39 | public let content:FlushableContentType? 40 | 41 | public convenience init(status:StatusCode, content:C? = nil, headers:Dictionary = Dictionary()) { 42 | self.init(status: status.rawValue, content: content, headers: headers) 43 | } 44 | 45 | public init(status:UInt16, content:C? = nil, headers:Dictionary = Dictionary()) { 46 | self.content = content 47 | super.init(status: status, headers:Response.adjustHeaders(headers: headers, c: content)) 48 | } 49 | 50 | public override func flushTo(out:DataConsumerType) -> Future { 51 | let content = self.content 52 | return super.flushTo(out: out).flatMap { ()->Future in 53 | return content.map {$0.flushTo(out: out)} ?? Future(value: ()) 54 | }.flatMap { ()->Future in 55 | return future(context: immediate) { 56 | try out.dataEnd() 57 | } 58 | } 59 | } 60 | 61 | static func adjustHeaders(headers:Dictionary, c:ContentType?) -> Dictionary { 62 | let cType:String? = c.flatMap { content in 63 | content.contentType 64 | } 65 | let h:Dictionary? = cType.map { ct in 66 | var mHeaders = headers 67 | mHeaders.updateValue(ct, forKey: HttpHeader.ContentType.rawValue) 68 | return mHeaders 69 | } 70 | return h.getOrElse(el: headers) 71 | } 72 | } 73 | 74 | extension Response where C : FlushableContent { 75 | convenience init(status:StatusCode, content:FlushableContentType?, headers:Dictionary = Dictionary()) { 76 | let content = content.map { content in 77 | C(content: content) 78 | } 79 | self.init(status: status, content: content, headers: headers) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Express/Router.swift: -------------------------------------------------------------------------------- 1 | //===--- Router.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 | protocol RouteType { 25 | var id:String {get} 26 | var matcher:UrlMatcherType {get} 27 | var factory:TransactionFactory {get} 28 | } 29 | 30 | class Route : RouteType { 31 | let id:String 32 | let matcher:UrlMatcherType 33 | let factory:TransactionFactory 34 | 35 | init(id:String, matcher:UrlMatcherType, factory:@escaping TransactionFactory) { 36 | self.id = id 37 | self.matcher = matcher 38 | self.factory = factory 39 | } 40 | } 41 | 42 | protocol RouterType { 43 | var routes:Array {get} 44 | 45 | func routeForId(id:String) -> RouteType? 46 | } 47 | -------------------------------------------------------------------------------- /Express/Server.swift: -------------------------------------------------------------------------------- 1 | //===--- Server.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 Result 25 | 26 | public protocol ServerType { 27 | var port:UInt16 {get} 28 | 29 | var app:Express { 30 | get 31 | } 32 | 33 | func start() -> Future 34 | 35 | init(app:Express, port:UInt16) 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Express/Static.swift: -------------------------------------------------------------------------------- 1 | //===--- Static.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 StaticDataProviderType { 27 | func etag(file:String) -> Future 28 | func data(app:Express, file:String) -> Future 29 | } 30 | 31 | public class StaticFileProvider : StaticDataProviderType { 32 | let root:String 33 | let fm = FileManager.default 34 | 35 | public init(root:String) { 36 | self.root = root 37 | } 38 | 39 | func fullPath(file:String) -> String { 40 | return root.bridge().appendingPathComponent(file) 41 | } 42 | 43 | private func attributes(file:String) throws -> [FileAttributeKey : Any] { 44 | do { 45 | 46 | return try self.fm.attributesOfItem(atPath: file) 47 | } catch { 48 | throw ExpressError.FileNotFound(filename: file) 49 | } 50 | } 51 | 52 | public func etag(file:String) -> Future { 53 | let file = fullPath(file: file) 54 | 55 | return future { 56 | let attributes = try self.attributes(file: file) 57 | 58 | guard let modificationDate = (attributes[FileAttributeKey.modificationDate].flatMap{$0 as? NSDate}) else { 59 | //TODO: throw different error 60 | throw ExpressError.PageNotFound(path: file) 61 | } 62 | 63 | let timestamp = UInt64(modificationDate.timeIntervalSince1970 * 1000 * 1000) 64 | 65 | //TODO: use MD5 of fileFromURI + timestamp 66 | let etag = "\"" + String(timestamp) + "\"" 67 | 68 | return etag 69 | } 70 | } 71 | 72 | public func data(app:Express, file:String) -> Future { 73 | let file = fullPath(file: file) 74 | 75 | return future { 76 | var isDir = ObjCBool(false) 77 | if !self.fm.fileExists(atPath: file, isDirectory: &isDir) || isDir.boolValue { 78 | //TODO: implement file directory index (WebDav) 79 | throw ExpressError.PageNotFound(path: file) 80 | } 81 | 82 | //TODO: get rid of NS 83 | guard let data = NSData(contentsOfFile: file) else { 84 | throw ExpressError.FileNotFound(filename: file) 85 | } 86 | 87 | let count = data.length / MemoryLayout.size 88 | // create array of appropriate length: 89 | var array = [UInt8](repeating: 0, count: count) 90 | 91 | // copy bytes into array 92 | data.getBytes(&array, length:count * MemoryLayout.size) 93 | 94 | let ext = file.bridge().pathExtension 95 | 96 | guard let content = AnyContent(data: array, contentType: MIME.extMime[ext]) else { 97 | throw ExpressError.FileNotFound(filename: file) 98 | } 99 | 100 | return content 101 | } 102 | } 103 | } 104 | 105 | public class BaseStaticAction : Action, IntermediateActionType { 106 | let param:String 107 | let dataProvider:StaticDataProviderType 108 | let cacheControl:CacheControl 109 | let headers:[String: String] 110 | 111 | public init(param:String, dataProvider:StaticDataProviderType, cacheControl:CacheControl = .NoCache) { 112 | self.param = param 113 | self.dataProvider = dataProvider 114 | self.cacheControl = cacheControl 115 | 116 | var headers = [String: String]() 117 | headers.updateWithHeader(header: self.cacheControl) 118 | 119 | self.headers = headers 120 | } 121 | 122 | public func nextAction(request:Request) -> Future<(AbstractActionType, Request?)> { 123 | 124 | if request.method != HttpMethod.Get.rawValue { 125 | return Future<(AbstractActionType, Request?)>(value: (Action.chain(), nil)) 126 | } 127 | 128 | guard let fileFromURI = request.params[self.param] else { 129 | print("Can not find ", self.param, " group in regex") 130 | return Future<(AbstractActionType, Request?)>(value: (Action.chain(), nil)) 131 | } 132 | 133 | let etag = self.dataProvider.etag(file: fileFromURI) 134 | 135 | return etag.flatMap { etag -> Future<(AbstractActionType, Request?)> in 136 | let headers = self.headers ++ ["ETag": etag] 137 | 138 | if let requestETag = request.headers["If-None-Match"] { 139 | if requestETag == etag { 140 | let action = Action.response(status: .NotModified, content: nil, headers: headers) 141 | return Future<(AbstractActionType, Request?)>(value: (action, nil)) 142 | } 143 | } 144 | 145 | let content = self.dataProvider.data(app: request.app, file: fileFromURI) 146 | 147 | return content.map { content in 148 | let flushableContent = FlushableContent(content: content) 149 | 150 | return (Action.ok(flushableContent, headers: headers), nil) 151 | } 152 | }.recoverWith { e in 153 | switch e { 154 | case ExpressError.PageNotFound(path: _): fallthrough 155 | case ExpressError.FileNotFound(filename: _): 156 | return Future(value: (Action.chain(), nil)) 157 | default: 158 | return Future(error: e) 159 | } 160 | } 161 | } 162 | } 163 | 164 | public class StaticAction : BaseStaticAction { 165 | 166 | public init(path:String, param:String, cacheControl:CacheControl = .NoCache) { 167 | let dataProvider = StaticFileProvider(root: path) 168 | super.init(param: param, dataProvider: dataProvider, cacheControl: cacheControl) 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /Express/StatusCode.swift: -------------------------------------------------------------------------------- 1 | //===--- StatusCode.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 enum StatusCode : UInt16 { 25 | case Accepted = 202 26 | case BadRequest = 400 27 | case Conflict = 409 28 | case Created = 201 29 | case EntityTooLarge = 413 30 | case ExpectationFailed = 417 31 | case Forbidden = 403 32 | case Found = 302 33 | case Gone = 410 34 | case InternalServerError = 500 35 | case MethodNotAllowed = 405 36 | case MovedPermanently = 301 37 | case NoContent = 204 38 | case NonAuthoritativeInformation = 203 39 | case NotAcceptable = 406 40 | case NotFound = 404 41 | case NotImplemented = 501 42 | case NotModified = 304 43 | case Ok = 200 44 | case PartialContent = 206 45 | case PreconditionFailed = 412 46 | case RequestTimeout = 408 47 | case ResetContent = 205 48 | case SeeOther = 303 49 | case ServiceUnavailable = 503 50 | case TemporaryRedirect = 307 51 | case TooManyRequest = 429 52 | case Unauthorized = 401 53 | case UnsupportedMediaType = 415 54 | case UnavailableForLegalReasons = 451 //farenheit 55 | case UriTooLong = 414 56 | } 57 | 58 | public enum RedirectStatusCode : UInt16 { 59 | case MovedPermanently = 301 60 | case Found = 302 61 | case SeeOther = 303 62 | case TemporaryRedirect = 307 63 | } -------------------------------------------------------------------------------- /Express/StencilViewEngine.swift: -------------------------------------------------------------------------------- 1 | //===--- StencilViewEngine.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 PathKit 24 | import Stencil 25 | 26 | private extension Path { 27 | var containerDir: Path { 28 | return Path(NSString(string: String(describing: self)).deletingLastPathComponent) 29 | } 30 | } 31 | 32 | typealias StencilEdible = [String: Any] 33 | 34 | private protocol StencilCookable { 35 | func cook() -> StencilEdible 36 | } 37 | 38 | private protocol StencilNormalizable { 39 | func normalizeValue(value:Any) -> Any 40 | func normalize() -> Any 41 | } 42 | 43 | extension StencilNormalizable { 44 | func normalizeValue(value:Any) -> Any { 45 | let normalizable = value as? StencilNormalizable 46 | return normalizable.map {$0.normalize()} .getOrElse(el: value) 47 | } 48 | } 49 | 50 | extension Array : StencilNormalizable { 51 | func normalize() -> Any { 52 | return self.map(normalizeValue) 53 | } 54 | } 55 | 56 | extension Dictionary : StencilNormalizable { 57 | func normalize() -> Any { 58 | return self.map { (k, v) in 59 | (k, normalizeValue(value: v)) 60 | } 61 | } 62 | } 63 | 64 | extension Dictionary : StencilCookable { 65 | func cook() -> StencilEdible { 66 | return self.map { (k,v) in 67 | return (String(describing: k), normalizeValue(value: v)) 68 | } 69 | } 70 | } 71 | 72 | private let loaderKey = "loader" 73 | 74 | class StencilView : ViewType { 75 | let template:Template 76 | 77 | 78 | init(template:Template, loader:Loader? = nil) { 79 | self.template = template 80 | 81 | 82 | } 83 | 84 | func render(context:C?) throws -> FlushableContentType { 85 | do { 86 | let edibleOption = context.flatMap{$0 as? StencilCookable }?.cook() 87 | let contextSupplied:[String:Any] = edibleOption.getOrElse(el: Dictionary()) 88 | 89 | let loader = contextSupplied.findFirst { (k, v) in 90 | k == loaderKey 91 | }.map{$1} 92 | 93 | if let loader = loader { 94 | guard let loader = loader as? Loader else { 95 | throw ExpressError.Render(description: "'loader' is a reserved key and can be of TemplateLoader type only", line: nil, cause: nil) 96 | } 97 | print("OK, loader: ", loader) 98 | //TODO: merge loaders 99 | } 100 | 101 | //let contextLoader:[String:Any] = self.loader.map{["loader": $0]}.getOrElse(el: Dictionary()) 102 | let finalContext = contextSupplied //++ contextLoader 103 | 104 | let render = try template.render(finalContext) 105 | 106 | 107 | return AnyContent(str:render, contentType: "text/html")! 108 | } catch let e as TemplateSyntaxError { 109 | throw ExpressError.Render(description: e.description, line: nil, cause: e) 110 | } 111 | } 112 | } 113 | 114 | public class StencilViewEngine : ViewEngineType { 115 | public init() { 116 | } 117 | 118 | public func extensions() -> Array { 119 | return ["stencil"] 120 | } 121 | 122 | public func view(filePath:String) throws -> ViewType { 123 | do { 124 | let path = Path(filePath) 125 | let dir = path.containerDir 126 | let loader = FileSystemLoader(paths: [dir]) 127 | 128 | let environment = Environment(loader: loader) 129 | 130 | let template = try environment.loadTemplate(name: path.lastComponent) 131 | 132 | return StencilView(template: template, loader: loader) 133 | } catch let e as TemplateSyntaxError { 134 | throw ExpressError.Render(description: e.description, line: nil, cause: e) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Express/Streams+Headers.swift: -------------------------------------------------------------------------------- 1 | //===--- Streams+Headers.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 | protocol ResponseHeadDataConsumerType : DataConsumerType { 26 | func consume(head:HttpResponseHeadType) -> Future 27 | } 28 | -------------------------------------------------------------------------------- /Express/Streams.swift: -------------------------------------------------------------------------------- 1 | //===--- Streams.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 DataConsumerType { 26 | func consume(data:Array) -> Future 27 | func dataEnd() throws 28 | } 29 | 30 | public protocol FlushableType { 31 | func flushTo(out:DataConsumerType) -> Future 32 | } 33 | -------------------------------------------------------------------------------- /Express/Transaction.swift: -------------------------------------------------------------------------------- 1 | //===--- Transaction.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 | import Result 26 | 27 | public protocol TransactionType : DataConsumerType { 28 | func tryConsume(content:ContentType) -> Bool 29 | 30 | var action:Future {get} 31 | 32 | func selfProcess() 33 | } 34 | 35 | class Transaction : TransactionType { 36 | let app:Express 37 | let routeId:String 38 | let out:DataConsumerType 39 | let head:RequestHeadType 40 | let factory:RequestContent.Factory 41 | let content:Future 42 | let actionPromise:Promise 43 | let action:Future 44 | let request:Promise> 45 | 46 | internal required init(app:Express, routeId:String, head:RequestHeadType, out:DataConsumerType) { 47 | self.app = app 48 | self.routeId = routeId 49 | self.out = out 50 | self.head = head 51 | self.factory = RequestContent.Factory(response: head) 52 | self.content = factory.content() 53 | self.actionPromise = Promise() 54 | self.action = actionPromise.future 55 | self.request = Promise>() 56 | content.settle(in: ExecutionContext.user).onSuccess() { content in 57 | let request = Request(app: app, head: head, body: content as? RequestContent) 58 | self.request.trySuccess(value: request) 59 | } 60 | content.onFailure { e in 61 | self.actionPromise.tryFail(error: e) 62 | } 63 | } 64 | 65 | convenience init(app:Express, routeId:String, head:RequestHeadType, out:DataConsumerType, handler:@escaping (Request) -> Future>) { 66 | self.init(app: app, routeId: routeId, head: head, out: out) 67 | request.future.onSuccess { request in 68 | let action = handler(request) 69 | action.onSuccess { action in 70 | self.actionPromise.trySuccess(value: action) 71 | } 72 | action.onFailure { e in 73 | self.failAction(e: e) 74 | } 75 | } 76 | } 77 | 78 | func failAction(e:Error) { 79 | self.actionPromise.tryFail(error: e) 80 | } 81 | 82 | func handleActionWithRequest(actionAndRequest:Future<(AbstractActionType, Request?)>) { 83 | actionAndRequest.onComplete { result in 84 | let action = Future(result: result.map {$0.0}) 85 | self.handleAction(action: action, request: result.value?.1) 86 | } 87 | } 88 | 89 | func handleAction(action:Future, request:Request?) { 90 | action.settle(in:ExecutionContext.action ).onSuccess{ action in 91 | if let request = request { 92 | self.processAction(action: action, request: request) 93 | } else { 94 | //yes we certainly have request here 95 | 96 | self.request.future.onSuccess{value in 97 | self.processAction(action: action, request: value) 98 | } 99 | } 100 | } 101 | action.onFailure { e in 102 | //yes, we always have at least the default error handler 103 | let next = self.app.errorHandler.handle(e: e)! 104 | 105 | if let request = request { 106 | self.processAction(action: next, request: request) 107 | } else { 108 | self.request.future.onSuccess { request in 109 | self.processAction(action: next, request: request) 110 | } 111 | } 112 | } 113 | } 114 | 115 | func selfProcess() { 116 | handleAction(action: action, request: Optional>.none) 117 | } 118 | 119 | func processAction(action:AbstractActionType, request:Request) { 120 | switch action { 121 | case let flushableAction as FlushableAction: flushableAction.flushTo(out: out) 122 | case let intermediateAction as IntermediateActionType: 123 | let actAndReq = intermediateAction.nextAction(request: request) 124 | handleActionWithRequest(actionAndRequest: actAndReq) 125 | case let selfSufficientAction as SelfSufficientActionType: 126 | selfSufficientAction.handle(app: app, routeId: routeId, request: request, out: out).onFailure { e in 127 | let action = Future(error: e) 128 | self.handleAction(action: action, request: request) 129 | } 130 | default: 131 | //TODO: handle server error 132 | print("wierd action... can do nothing with it") 133 | } 134 | } 135 | 136 | func tryConsume(content:ContentType) -> Bool { 137 | return factory.tryConsume(content: content) 138 | } 139 | 140 | func consume(data:Array) -> Future { 141 | return factory.consume(data: data) 142 | } 143 | 144 | func dataEnd() throws { 145 | try factory.dataEnd() 146 | } 147 | } 148 | 149 | typealias TransactionFactory = (RequestHeadType, DataConsumerType)->TransactionType 150 | -------------------------------------------------------------------------------- /Express/UrlMatcher.swift: -------------------------------------------------------------------------------- 1 | //===--- UrlMatcher.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 UrlMatcherType { 25 | /// 26 | /// Matches path with a route and returns matched params if avalable. 27 | /// - Parameter path: path to match over 28 | /// - Returns: nil if route does not match. Matched params otherwise 29 | /// 30 | func match(method:String, path:String) -> [String: String]? 31 | } 32 | 33 | extension RouterType { 34 | func nextRoute(index:Array.Index?, request:RequestHeadType?) -> (RouteType, [String: String])? { 35 | return request.flatMap { req in 36 | let url = req.path 37 | let method = req.method 38 | 39 | let route:(RouteType, [String: String])? = index.flatMap { i in 40 | let rest = routes.suffix(from: i) 41 | return rest.mapFirst { e in 42 | guard let match = e.matcher.match(method: method, path:url) else { 43 | return nil 44 | } 45 | return (e, match) 46 | } 47 | } 48 | return route 49 | } 50 | } 51 | 52 | func nextRoute(routeId:String, request:RequestHeadType?) -> (RouteType, [String: String])? { 53 | return request.flatMap { req in 54 | 55 | 56 | 57 | let index = routes.index {routeId == $0.id}.map { $0 + 1 } // $0.successor 58 | return nextRoute(index: index, request: request) 59 | } 60 | } 61 | 62 | func firstRoute(request:RequestHeadType?) -> (RouteType, [String: String])? { 63 | return nextRoute(index: routes.startIndex, request: request) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Express/Utils.swift: -------------------------------------------------------------------------------- 1 | //===--- Utils.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 | #if !os(Linux) 25 | public extension String { 26 | func bridge() -> NSString { 27 | return self as NSString 28 | } 29 | } 30 | #endif -------------------------------------------------------------------------------- /Express/View.swift: -------------------------------------------------------------------------------- 1 | //===--- View.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 ViewType { 25 | func render(context:Context?) throws -> FlushableContentType 26 | } 27 | 28 | public protocol NamedViewType : ViewType { 29 | var name:String {get} 30 | } -------------------------------------------------------------------------------- /Express/ViewEngine.swift: -------------------------------------------------------------------------------- 1 | //===--- ViewEngine.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 ViewEngineType { 25 | func extensions() -> Array 26 | func view(filePath:String) throws -> ViewType 27 | } -------------------------------------------------------------------------------- /Express/Views.swift: -------------------------------------------------------------------------------- 1 | //===--- Views.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 | import Result 26 | import Boilerplate 27 | 28 | public class Views { 29 | //TODO: move hardcode to config 30 | internal var views:Dictionary = Dictionary() 31 | internal var paths:Array = ["./views"] 32 | internal var engines:Dictionary = Dictionary() 33 | internal let viewContext = ExecutionContext.view 34 | internal let renderContext = ExecutionContext.render 35 | 36 | public var cache:Bool = false 37 | func cacheView(viewName:String, view:Future) -> Future { 38 | if cache { 39 | return view.settle(in: self.viewContext).onComplete { result in 40 | if let val = try? result.dematerialize() { 41 | self.views[viewName] = val 42 | } 43 | } 44 | } else { 45 | return view 46 | } 47 | } 48 | 49 | public func register(_ path:String) { 50 | future(context: viewContext) { ()->Void in 51 | self.paths.append(path) 52 | } 53 | } 54 | 55 | public func register(_ view: ViewType, name:String) { 56 | future(context: viewContext) { 57 | self.views[name] = view 58 | } 59 | } 60 | 61 | public func register(_ view: NamedViewType) { 62 | register(view, name: view.name) 63 | } 64 | 65 | public func register(_ engine: ViewEngineType) { 66 | future(context: viewContext) { 67 | let exts = engine.extensions() 68 | for ext in exts { 69 | self.engines[ext] = engine 70 | } 71 | } 72 | } 73 | 74 | func view(viewName:String, resolver: @escaping (String)->Future) -> Future { 75 | return future(context: viewContext) { 76 | Result(value: self.views[viewName]) 77 | }.flatMap { (view:ViewType?) -> Future in 78 | return view.map { view in 79 | Future(value: view) 80 | }.getOrElse { 81 | return self.cacheView(viewName: viewName, view: resolver(viewName)) 82 | } 83 | } 84 | } 85 | 86 | func view(viewName:String) -> Future { 87 | return view(viewName: viewName) { viewName in 88 | 89 | return future(context: self.viewContext) { ()->Result in 90 | let fileManager = FileManager.default 91 | let exts = self.engines.keys 92 | 93 | let combinedData = self.paths.map { path in 94 | exts.map { ext in 95 | (ext, path.bridge().appendingPathComponent(viewName) + "." + ext) 96 | } 97 | }.joined() 98 | 99 | return combinedData.findFirst { (ext, file) -> Bool in 100 | // get first found template (ext, file) 101 | //TODO: (path as NSString).stringByAppendingPathComponent(view) reimplement 102 | var isDir = ObjCBool(false) 103 | return fileManager.fileExists(atPath: file, isDirectory: &isDir) && !isDir.boolValue 104 | }.flatMap { (ext, file) -> (ViewEngineType, String)? in 105 | //convert to engine and full file path 106 | let engine = self.engines[ext] 107 | return engine.map { (engine:ViewEngineType) -> (ViewEngineType, String) in 108 | (engine, file) 109 | } 110 | }.map { (engine, file) -> Result in 111 | do { 112 | return Result(value: try engine.view(filePath: file)) 113 | } catch let e as ExpressError { 114 | switch e { 115 | case ExpressError.FileNotFound(let filename): return Result(error: AnyError(ExpressError.NoSuchView(name: filename))) 116 | default: return Result(error: AnyError(e)) 117 | } 118 | } catch let e { 119 | return Result(error: AnyError(e)) 120 | } 121 | }.getOrElse(el: Result(error: AnyError(ExpressError.NoSuchView(name: viewName)))) 122 | 123 | 124 | 125 | } 126 | } 127 | } 128 | 129 | public func render(view:String, context:Context?) -> Future { 130 | return self.view(viewName: view).settle(in: viewContext).map { view in 131 | try view.render(context: context) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /LICENSE.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | //===--- Package.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 PackageDescription 23 | 24 | let package = Package( 25 | name: "Express", 26 | 27 | targets: [ 28 | Target( 29 | name: "Express" 30 | ), 31 | Target( 32 | name:"Demo", 33 | dependencies:[.Target(name:"Express")] 34 | ) 35 | ], 36 | dependencies: [ 37 | .Package(url: "https://github.com/reactive-swift/Future.git", majorVersion: 0, minor: 2), 38 | .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", majorVersion: 15), 39 | .Package(url: "https://github.com/crossroadlabs/PathToRegex.git", majorVersion: 0, minor: 4), 40 | .Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8), 41 | .Package(url: "https://github.com/IBM-Swift/GRMustache.swift", majorVersion: 1, minor: 5), 42 | .Package(url: "https://github.com/crossroadlabs/CEVHTP.git", majorVersion: 0, minor: 4), 43 | ], 44 | exclude: ["doc"] 45 | ) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: https://www.iconfinder.com/icons/383207/doc_tag_icon#size=64 2 | 7 | 8 |

    9 |

    10 | Twitter 11 | Facebook 12 | LinkedIn 13 | Web site 14 | Slack 15 |

    16 |

    17 |

    Documentation
    18 | 19 |
    Live 🐧 server running Demo
    20 | 21 |
    Eating our own dog food
    22 |
    23 |

    24 |

    25 | 26 | ![🐧 linux: ready](https://img.shields.io/badge/%F0%9F%90%A7%20linux-ready-red.svg) 27 | [![Build Status](https://travis-ci.org/crossroadlabs/Express.svg?branch=master)](https://travis-ci.org/crossroadlabs/Express) 28 | [![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) 29 | ![Platform OS X | Linux](https://img.shields.io/badge/platform-OS%20X%20%7C%20Linux-orange.svg) 30 | ![Swift version](https://img.shields.io/badge/Swift-3.x-blue.svg) 31 | [![GitHub license](https://img.shields.io/badge/license-LGPL v3-green.svg)](https://raw.githubusercontent.com/crossroadlabs/Express/master/LICENSE) 32 | [![GitHub release](https://img.shields.io/github/release/crossroadlabs/Express.svg)](https://github.com/crossroadlabs/Express/releases) 33 | 34 | 35 | 38 |
    36 | UPDATE March 2017: Swift Express is back on track and is Swift 3.x compatible. From now on we will suport the latest stable swift builds only. There is no release of the newest version done yet, though you can enjoy it from the "master" branch. Stay tuned. 37 |
    39 | 40 | 41 | 44 |
    42 | Version 0.3.x (current stable) notice: Current version works with Xcode 7.2, 7.3 and Linux DEV SNAPSHOT released on 03.01.2016. Upcoming version (0.4.x) will fully support Swift 3.0 and will maintain compatibility with Swift 2.2 (Xcode 7.3). Stay tuned by following us on social networks. 43 |
    45 | 46 | ### Being [perfectionists](http://www.crossroadlabs.xyz), we took the best from what we think is the best: power of [Play Framework](https://www.playframework.com/) and simplicity of [Express.js](http://expressjs.com/) 47 | 48 | #### Express is an asynchronous, simple, powerful, yet unopinionated web application server written in Swift 49 | 50 | ## Getting started 51 | 52 | First make sure, please, you have followed the [installation](#installation) section steps. 53 | 54 | ##### Create a project: 55 | 56 | ```sh 57 | swift-express init HelloExpress 58 | cd HelloExpress 59 | swift-express bootstrap 60 | open HelloExpress.xcodeproj 61 | ``` 62 | 63 | ##### Create new API: 64 | 65 | ```swift 66 | app.get("/myecho") { request in 67 | .ok(request.query["message"]?.first) 68 | } 69 | ``` 70 | 71 | ##### Run from xCode or command line with: 72 | 73 | ```sh 74 | swift-express build 75 | swift-express run 76 | ``` 77 | 78 | Test it in the browser: [http://localhost:9999/myecho?message=Hello](http://localhost:9999/myecho?message=Hello) 79 | 80 | ##### A complete Swift Express command line documentation can be found here: [https://github.com/crossroadlabs/ExpressCommandLine](https://github.com/crossroadlabs/ExpressCommandLine) 81 | 82 | 83 | ## Installation 84 | 85 | [//]: # (Icons are here: https://www.iconfinder.com/icons/395228/linux_tox_icon#size=16) 86 | 87 | ### [OS X ![OS X](https://cdn1.iconfinder.com/data/icons/system-shade-circles/512/mac_os_X-16.png)](http://www.apple.com/osx/) 88 | 89 | ##### First install the following components (if you have not yet): 90 | 91 | * [XCode](https://developer.apple.com/xcode/download/) 7.2 or higher 92 | * [Homebrew](http://brew.sh/) the latest available version 93 | * Command Line tools: run ```xcode-select --install``` in terminal 94 | 95 | ##### Run the following in terminal: 96 | 97 | ```sh 98 | brew tap crossroadlabs/tap 99 | brew install swift-express 100 | ``` 101 | 102 | #### [Linux ![Linux](https://cdn1.iconfinder.com/data/icons/system-shade-circles/512/linux_tox-16.png)](http://www.linux.org/) 103 | 104 | ##### For instructions on how to get [Express](http://swiftexpress.io/) installed on Linux, please, refer to the [installation section](./doc/gettingstarted/installing.md#linux-) in the [ducumentation](./doc/index.md). 105 | 106 | ## Examples 107 | 108 | Create a project as it is described in the [getting started](#getting-started) section. Now you can start playing with examples. 109 | 110 | All the examples can be found in `Demo` project inside the main repo. 111 | 112 | ### Hello Express: 113 | 114 | ```swift 115 | app.get("/hello") { request in 116 | .ok(AnyContent(str: "

    Hello Express!!!

    ", contentType: "text/html")) 117 | } 118 | ``` 119 | 120 | Launch the app and follow the link: [http://localhost:9999/hello?message=Hello](http://localhost:9999/hello?message=Hello) 121 | 122 | ### Synchronous vs Asynchronous 123 | 124 | If you don't know what this is you might want to better skip it for now to the next section: [URL params](#url-params). To get more information see [this](http://cs.brown.edu/courses/cs168/s12/handouts/async.pdf) first. We have our APIs based on [Future pattern](https://en.wikipedia.org/wiki/Futures_and_promises). Our implementation is based on [BrightFutures](https://github.com/Thomvis/BrightFutures), thanks @Thomvis! 125 | 126 | Express can handle it both ways. All your syncronous code will be executed in a separate queue in a traditional way, so if you are a fan of this approach - it will work (like in "Hello Express" example above). 127 | 128 | Still if you want to benefit from asynchronicity, we provide a very powerful API set that accepts futures as result of your handler. 129 | 130 | Let's assume you have following function somewhere: 131 | 132 | ```swift 133 | func calcFactorial(num:Double) -> Future 134 | ``` 135 | 136 | it's a purely asyncronous function that returns future. It would be really nice if it could be handled asynchronously as well in a nice functional way. Here is an example of how it could be done. 137 | 138 | 139 | ```swift 140 | // (request -> Future, AnyError> in) - this is required to tell swift you want to return a Future 141 | // hopefully inference in swift will get better eventually and just "request in" will be enough 142 | app.get("/factorial/:num(\\d+)") { request -> Future, AnyError> in 143 | // get the number from the url 144 | let num = request.params["num"].flatMap{Double($0)}.getOrElse(0) 145 | 146 | // get the factorial Future. Returns immediately - non-blocking 147 | let factorial = calcFactorial(num) 148 | 149 | //map the result of future to Express Action 150 | let future = factorial.map { fac in 151 | Action.ok(String(fac)) 152 | } 153 | 154 | //return the future 155 | return future 156 | } 157 | ``` 158 | 159 | ### URL params 160 | 161 | Let's get our echo example from [Getting Started](#getting-started) a bit further. Our routing engine, which is largely based on NodeJS analog [path-to-regex](https://github.com/pillarjs/path-to-regexp). You can read the complete documentation on how to use path patterns [here](https://github.com/pillarjs/path-to-regexp). Now an example with URL param: 162 | 163 | ```swift 164 | //:param - this is how you define a part of URL you want to receive through request object 165 | app.get("/echo/:param") { request in 166 | //here you get the param from request: request.params["param"] 167 | .ok(request.params["param"]) 168 | } 169 | ``` 170 | 171 | ### Serving static files 172 | 173 | ```swift 174 | app.get("/:file+", action: StaticAction(path: "public", param:"file")) 175 | ``` 176 | 177 | The code above tells Express to serve all static files from the public folder recursively. If you want to serve just the first level in folder, use: 178 | 179 | ```swift 180 | app.get("/:file", action: StaticAction(path: "public", param:"file")) 181 | ``` 182 | 183 | The difference is just in the pattern: `/:file` versus `/:file+`. For more information see our routing section. 184 | 185 | ### Serving JSON requests 186 | 187 | First of all we need to register the JSON view in the system: 188 | 189 | ```swift 190 | //now we can refer to this view by name 191 | app.views.register(JsonView()) 192 | ``` 193 | 194 | Let's say we want to build a simple API for users registration. We want our API consumers to `POST` to `/api/user` a JSON object and get a `JSON` response back. 195 | 196 | ```swift 197 | app.post("/api/user") { request in 198 | //check if JSON has arrived 199 | guard let json = request.body?.asJSON() else { 200 | return Action.ok("Invalid request") 201 | } 202 | //check if JSON object has username field 203 | guard let username = json["username"].string else { 204 | return Action.ok("Invalid request") 205 | } 206 | //compose the response as a simple dictionary 207 | let response = 208 | ["status": "ok", 209 | "description": "User with username '" + username + "' created succesfully"] 210 | 211 | //render disctionary as json (remember the one we've registered above?) 212 | return .render(JsonView.name, context: response) 213 | } 214 | ``` 215 | 216 | Lines above will do the job. Post this `JSON`: 217 | 218 | ```json 219 | { 220 | "username": "swiftexpress" 221 | } 222 | ``` 223 | 224 | to our api URL: `http://localhost:9999/api/user` (don't forget `application/json` content type header) and you will get this response: 225 | 226 | ```json 227 | { 228 | "status": "ok", 229 | "description": "User with username 'swiftexpress' created succesfully" 230 | } 231 | ``` 232 | 233 | ### Using template engine 234 | 235 | First of all you need to switch the template engine on: 236 | 237 | ```swift 238 | //we recommend mustache template engine 239 | app.views.register(StencilViewEngine()) 240 | ``` 241 | 242 | Now create a file called `hello.stencil` in the `views` directory: 243 | 244 | ```stencil 245 | 246 | 247 |

    Hello from Stencil: {{user}}

    248 | 249 | 250 | ``` 251 | 252 | Add a new request handler: 253 | 254 | ```swift 255 | //user as an url param 256 | app.get("/hello/:user.html") { request in 257 | //get user 258 | let user = request.params["user"] 259 | //if there is a user - create our context. If there is no user, context will remain nil 260 | let context = user.map {["user": $0]} 261 | //render our template named "hello" 262 | return .render("hello", context: context) 263 | } 264 | ``` 265 | 266 | Now follow the link to see the result: [http://localhost:9999/hello/express.html](http://localhost:9999/hello/express.html) 267 | 268 | 269 | ### If you want more, please, visit our [documentation](./doc/index.md) page 270 | 271 | ## Ideology behind 272 | 273 | ### Taking the best of Swift 274 | 275 | [Swift](https://swift.org/) essentially is a new generation programming language combining simplicity and all the modern stuff like functional programming. 276 | 277 | We were inspired (and thus influenced) mainly by two modern web frameworks: [Express.js](http://expressjs.com/) and [Play](https://www.playframework.com/). So, we are trying to combine the best of both worlds taking simplicity from [Express.js](http://expressjs.com/) and modern robust approach of [Play](https://www.playframework.com/) 278 | 279 | Let us know if we are on the right path! Influence the project, create feature requests, API change requests and so on. While we are in our early stages, it's easy to change. We are open to suggestions! 280 | 281 | ## Features 282 | 283 | * 🐧 Linux support with and without [Dispatch](https://swift.org/core-libraries/#libdispatch) 284 | * 100% asynchronous (Future-based API) 285 | * Flexible and extensible 286 | * Full [MVC](https://ru.wikipedia.org/wiki/Model-View-Controller) support 287 | * Swift 2.1 and 2.2 compatible 288 | * [Simple routing mechanism](./doc/gettingstarted/routing.md) 289 | * Request handlers chaining 290 | * [Typesafe Error Handlers](./doc/gettingstarted/errorhandling.md) 291 | * Templates: [Stencil](https://github.com/kylef/Stencil) and [Mustache](https://mustache.github.io) 292 | * Built-in [JSON](http://www.json.org) support 293 | * Easy creation of [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) APIs 294 | * Built-in [static files serving](./doc/gettingstarted/static.md) 295 | * Multiple contents types built-in support 296 | 297 | ### And heah, the most important feature: [Highly passionate development team](http://www.crossroadlabs.xyz/) 298 | 299 | ## Roadmap 300 | 301 | * v0.4: stable version with Swift 3.0 302 | * v0.5: proper streaming 303 | * v0.6: new core (based on [Reactive Swift](https://github.com/reactive-swift)) 304 | * v0.7: more content types available out of the box 305 | * v0.8: Web Sockets 306 | * v0.9: hot code reload 307 | * v1.0: hit the production! 308 | 309 | ## Changelog 310 | 311 | * v0.4: Swift 3.0 312 | * Ported Express to Swift 3.0 313 | * Moved to [Reactive Swift](https://github.com/reactive-swift) foundation ([Execution contexts](https://github.com/reactive-swift/ExecutionContext), [Futures](https://github.com/reactive-swift/Future), etc.) 314 | * Wrapped libevent as an [ExecutionContext](https://github.com/reactive-swift/ExecutionContext) 315 | * Dropped [Carthage](https://github.com/Carthage/Carthage) support 316 | 317 | * v0.3: linux support 318 | * Runs on linux with and without [Dispatch](https://swift.org/core-libraries/#libdispatch) support (see [installation section](./doc/gettingstarted/installing.md#linux-) and [building in production](./doc/gettingstarted/buildrun.md#production-build)) 319 | * FormUrlEncoded ContentType support 320 | * Merged Query (params from both query string and form-url-encoded body merged together) 321 | * Utility methods (redirect, status, etc) 322 | * [Stencil](https://github.com/kylef/Stencil) Templete Engine Support 323 | * Replaced [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) with [TidyJSON](https://github.com/benloong/TidyJSON) 324 | * [Typesafe Error Handlers](./doc/gettingstarted/errorhandling.md) 325 | * Better Demo app 326 | * v0.2.1: minor changes 327 | * Swift modules are installed via Carthage 328 | * Enabled binary builds on OS X 329 | * v0.2: Solid OS X release 330 | * Much better routing APIs 331 | * Advanced routing path patterns 332 | * Possibility to use Regex for routing 333 | * Greately improved README 334 | * Some bugfixes 335 | * v0.1: Initial Public Release 336 | * basic routing 337 | * views and view engines (supports [Mustache](https://mustache.github.io/)) 338 | * JSON rendering as a view 339 | * query parsing 340 | * static files serving 341 | 342 | ## Contributing 343 | 344 | To get started, sign the Contributor License Agreement. 345 | 346 | ## [![Crossroad Labs](http://i.imgur.com/iRlxgOL.png?1) by Crossroad Labs](http://www.crossroadlabs.xyz/) 347 | -------------------------------------------------------------------------------- /doc/gettingstarted/buildrun.md: -------------------------------------------------------------------------------- 1 | # Building and running your [Express](http://swiftexpress.io/) app 2 | 3 | This section is dedicated to different types and flavors of building and running [Express](http://swiftexpress.io/) apps. Differences between Linux and OS X systems are outlined explicitly. 4 | 5 | ## Building with [Express Command Line](https://github.com/crossroadlabs/ExpressCommandLine) 6 | 7 | _At the moment of writing [Express Command Line](https://github.com/crossroadlabs/ExpressCommandLine) tools are available for OS X only, but soon to be ported to Linux as well. Stay tuned._ 8 | 9 | [Express Command Line](https://github.com/crossroadlabs/ExpressCommandLine) is the easiest way to work with [Express](http://swiftexpress.io/) apps. The building and running is as straightforward as: 10 | 11 | ```sh 12 | swift-express build 13 | swift-express run 14 | ``` 15 | 16 | For the full set of available parameters, please, refer to the main [documentation page](https://github.com/crossroadlabs/ExpressCommandLine). 17 | 18 | ## Building manually with [Swift Package Manager](https://github.com/apple/swift-package-manager) 19 | 20 | [Swift Package Manager](https://github.com/apple/swift-package-manager) is the default way of building Swift apps. Most probably soon to be supported in xCode out of the box as well. If you don't want to use [Express Command Line](https://github.com/crossroadlabs/ExpressCommandLine) for some reason, here is how to build an express app manually. 21 | 22 | ### Development build 23 | 24 | In development, it's generally not a good idea to build the app with [Dispatch](https://swift.org/core-libraries/#libdispatch) support on Linux as it becomes non-debuggable with lldb. To build an [Express](http://swiftexpress.io/) app without [Dispatch](https://swift.org/core-libraries/#libdispatch) run the following in terminal: 25 | 26 | ```sh 27 | swift build --fetch 28 | #swift build does not work with tests properly yet 29 | rm -rf Packages/*/Tests 30 | swift build 31 | ``` 32 | 33 | Now you can run your app with: 34 | 35 | ```sh 36 | # note, that if you use port 80 you might need to do it with sudo 37 | ./.build/debug/YOUR_APP_NAME 38 | ``` 39 | 40 | ### Production build 41 | 42 | [Express](http://swiftexpress.io/) apps show a level of magnitude better performance when built with dispatch support. If you are using Linux, before continuing make sure you followed [Dispatch installation section](./installing.md#installing-dispatch-on-linux). 43 | 44 | Here is how to build your app for production: 45 | 46 | ```sh 47 | swift build --fetch 48 | #swift build does not work with tests properly yet 49 | rm -rf Packages/*/Tests 50 | swift build -c release -Xcc -fblocks -Xswiftc -Ddispatch 51 | ``` 52 | 53 | Now you can run your app in production with: 54 | 55 | ```sh 56 | # note, that if you use port 80 you might need to do it with sudo 57 | ./.build/release/YOUR_APP_NAME 58 | ``` 59 | 60 | # Next tutorial: [Basic Routing](./routing.md) -------------------------------------------------------------------------------- /doc/gettingstarted/commandline.md: -------------------------------------------------------------------------------- 1 | # Express Command Line 2 | 3 | 4 | 11 |
    5 |

    6 | 7 | Note: Express Command Line tool is not yet officially supported on Linux. This tutorial works on OS X only. 8 | 9 |

    10 |
    12 | 13 | Using [Express](http://swiftexpress.io) is rather easy. To create a project you just need to type: 14 | 15 | ```sh 16 | swift-express init YourProject 17 | ``` 18 | 19 | ant it will create you the whole directory structure along with xCode project. 20 | 21 | No you need to fetch and build the dependencies: 22 | 23 | ```sh 24 | cd YourProject 25 | swift-express bootstrap 26 | ``` 27 | 28 | Build the project: 29 | 30 | ```sh 31 | swift-express build 32 | ``` 33 | 34 | You can run your project by typing following test while in the project folder: 35 | 36 | ```sh 37 | swift-express run 38 | ``` 39 | 40 | Full documentation of [Express Command Line](https://github.com/crossroadlabs/ExpressCommandLine) can be found here [crossroadlabs/ExpressCommandLine](https://github.com/crossroadlabs/ExpressCommandLine). 41 | 42 | # Next tutorial: [Building and running](./buildrun.md) 43 | 44 | -------------------------------------------------------------------------------- /doc/gettingstarted/errorhandling.md: -------------------------------------------------------------------------------- 1 | # Basic error handling 2 | 3 | If you want to generate an error in [Express](http://swiftexpress.io/) you need to throw an exception. 4 | 5 | Let's say you have a `NastyError` defined: 6 | 7 | ```swift 8 | enum NastyError : ErrorType { 9 | case Recoverable 10 | case Fatal(reason:String) 11 | } 12 | ``` 13 | 14 | Here is an example on how to throw it: 15 | 16 | ```swift 17 | app.get("/error/:fatal?") { request in 18 | guard let fatal = request.params["fatal"] else { 19 | throw NastyError.Recoverable 20 | } 21 | 22 | throw NastyError.Fatal(reason: fatal) 23 | } 24 | ``` 25 | 26 | Now how to handle it. In [Express](http://swiftexpress.io/) you can define error handlers of two types. General and specific. Specific are the ones with the specified `Error` type. In our case it's `NastyError`. Here is an example: 27 | 28 | ```swift 29 | app.errorHandler.register { (e:NastyError) in 30 | switch e { 31 | case .Recoverable: 32 | return Action.redirect("/") 33 | case .Fatal(let reason): 34 | let content = AnyContent(str: "Unrecoverable nasty error happened. Reason: " + reason) 35 | return Action.response(.InternalServerError, content: content) 36 | } 37 | } 38 | ``` 39 | 40 | General error handlers are the same except you omit the error type. Something like this: 41 | 42 | ```swift 43 | app.errorHandler.register { e in 44 | return nil 45 | } 46 | ``` 47 | 48 | If your handler can handle the error, you return an `Action`. Otherwise you return `nil`. Here is an example of selective error handling: 49 | 50 | ```swift 51 | /// Custom page not found error handler 52 | app.errorHandler.register { (e:ExpressError) in 53 | switch e { 54 | case .PageNotFound(let path): 55 | return Action.render("404", context: ["path": path], status: .NotFound) 56 | default: 57 | return nil 58 | } 59 | } 60 | ``` 61 | 62 | 63 | # Next tutorial: [Advanced something](#) -------------------------------------------------------------------------------- /doc/gettingstarted/helloexpress.md: -------------------------------------------------------------------------------- 1 | # Hello Express 2 | 3 | This is a very basic application. Much simpler than one created with [Express Command Line](./commandline.md). Still it's good to understand the basic concepts by creating an app by hands. Keep in mind that you have to follow [Installation instructions](./installing.md) first. If you are doing it on a Mac, please, install [Swift 2.2 latest development](https://swift.org/download/#latest-development-snapshots) snapshot in addition. 4 | 5 | ##### If you don't want to create an app manually, just skip this tutorial and use [Express Command Line](./commandline.md) tool instead. 6 | 7 | ## Create a folder 8 | 9 | Simple as that. We need a separate folder for the [Hello Express](#) app. Just run: 10 | 11 | ```sh 12 | mkdir HelloExpress 13 | cd HelloExpress 14 | ``` 15 | 16 | ## Package.swift 17 | 18 | [Package.swift](https://github.com/apple/swift-package-manager/blob/master/Documentation/Package.swift.md) is your project descriptor. Contains the name of the app and dependencies. Check [reference](https://github.com/apple/swift-package-manager/blob/master/Documentation/Package.swift.md) for more info. 19 | 20 | Create one right in the current dirctory and put the following text inside (we have just one dependency to [Express](http://www.swiftexpress.io/)): 21 | 22 | ```swift 23 | import PackageDescription 24 | 25 | let package = Package( 26 | name: "HelloExpress", 27 | dependencies: [ 28 | .Package(url: "https://github.com/crossroadlabs/Express.git", majorVersion: 0, minor: 3), 29 | ] 30 | ) 31 | ``` 32 | 33 | ## The App 34 | 35 | Create a folder `app` in current directory and put there a file named `main.swift`. The contents of the file should look like: 36 | 37 | ```swift 38 | import Express 39 | 40 | let app = express() 41 | 42 | app.get("/") { request in 43 | return Action.ok("Hello Express!") 44 | } 45 | 46 | app.listen(9999).onSuccess { server in 47 | print("Express was successfully launched on port", server.port) 48 | } 49 | 50 | app.run() 51 | ``` 52 | 53 | This means that all the requests coming to the root will be responded with _Hello Express!_ string. All other requests will get `404`. 54 | 55 | ## Build tool 56 | 57 | Swift on Linux and [Package Manager](https://github.com/apple/swift-package-manager) are early tools and are a bit complicated to use. We created a simple tool to build Swift on Linux. Get it using following commands: 58 | 59 | ```sh 60 | wget https://raw.githubusercontent.com/crossroadlabs/utils/master/build 61 | chmod a+x build 62 | ``` 63 | 64 | ## Build 65 | 66 | Just type in terminal: 67 | 68 | ```sh 69 | ./build 70 | ``` 71 | 72 | ## Run 73 | 74 | Run our new app with: 75 | 76 | ```sh 77 | ./.build/debug/app 78 | ``` 79 | 80 | In the console you should see something like this: 81 | 82 | ``` 83 | Express was successfully launched on port 9999 84 | ``` 85 | 86 | ## Test 87 | 88 | Enter the following url in the browser: [http://localhost:9999/](http://localhost:9999/). You should now see text: 89 | 90 | ``` 91 | http://localhost:9999/ 92 | ``` 93 | 94 | # Next tutorial: [Express Command Line](./commandline.md) -------------------------------------------------------------------------------- /doc/gettingstarted/installing.md: -------------------------------------------------------------------------------- 1 | #Installing 2 | 3 | ## [OS X ![OS X](https://cdn1.iconfinder.com/data/icons/system-shade-circles/512/mac_os_X-16.png)](http://www.apple.com/osx/) 4 | 5 | ##### First install the following components (if you have not yet): 6 | 7 | * [XCode](https://developer.apple.com/xcode/download/) 7.2 or higher 8 | * [Homebrew](http://brew.sh/) the latest available version 9 | * Command Line tools: run ```xcode-select --install``` in terminal 10 | 11 | ##### Run the following in terminal: 12 | 13 | ```sh 14 | brew tap crossroadlabs/tap 15 | brew install swift-express 16 | ``` 17 | 18 | ## [Linux ![Linux](https://cdn1.iconfinder.com/data/icons/system-shade-circles/512/linux_tox-16.png)](http://www.linux.org/) 19 | 20 | ##### First install the following components (if you have not yet): 21 | 22 | * [Linux](http://www.linux.org/), one of the following distributions will work: 23 | * [Ubuntu 15.10 (Wily Werewolf)](http://releases.ubuntu.com/15.10/) 24 | * [Ubuntu 14.04 (Trusty Tahr)](http://releases.ubuntu.com/14.04/) 25 | * We have exprerienced some dependencies missing if installing by original instructions from [swift.org](http://swift.org/). Install these dependencies first, please: 26 | 27 | ```sh 28 | apt-get install clang binutils libicu-dev 29 | ``` 30 | 31 | * [Swift](https://swift.org/), the latest development snapshot from [here](https://swift.org/download/#latest-development-snapshots) 32 | * _You should have swift at least of 25.02.2016_ 33 | * Installation instructions are [here](https://swift.org/getting-started/#on-linux) 34 | * Dependency libraries: 35 | 36 | If you are using Ubuntu 15.10, you are lucky. Skip the following step. If you are on Ubuntu 14.04 you need to add our repo to your `apt`: 37 | 38 | ```sh 39 | sudo add-apt-repository ppa:swiftexpress/swiftexpress 40 | sudo apt-get update 41 | ``` 42 | 43 | Following is common for both Ubuntu 14.04 and Ubuntu 15.10: 44 | 45 | ```sh 46 | sudo apt-get install libevhtp-dev libevent-dev libssl-dev git 47 | ``` 48 | 49 | * [Dispatch](https://swift.org/core-libraries/#libdispatch) _(optional)_ 50 | * For more information refer to the dedicated [Dispatch installation section](#installing-dispatch-on-linux), please. 51 | 52 | 53 | ##### We have not ported our command line tools to Linux yet, so either [generate project on OS X](#) and then use it or use [this](#) temporary script: 54 | 55 | ```sh 56 | TBD 57 | # download the script 58 | ``` 59 | 60 | ### Installing [Dispatch](https://swift.org/core-libraries/#libdispatch) on Linux 61 | 62 | Dispatch is not available as a prebuilt package yet, so we have to build it from sources: 63 | 64 | * Install prerequisites: 65 | 66 | ```sh 67 | sudo apt-get install autoconf libtool pkg-config systemtap-sdt-dev libblocksruntime-dev libkqueue-dev libbsd-dev git make 68 | ``` 69 | 70 | * Clone dispatch repository and get into it: 71 | 72 | ```sh 73 | git clone https://github.com/apple/swift-corelibs-libdispatch.git 74 | cd swift-corelibs-libdispatch 75 | ``` 76 | 77 | * Initialize submodules: 78 | 79 | ```sh 80 | git submodule init 81 | git submodule update 82 | ``` 83 | 84 | * Generate build toolset: 85 | 86 | ```sh 87 | sh ./autogen.sh 88 | ``` 89 | 90 | * Configure build: 91 | 92 | ```sh 93 | ./configure --with-swift-toolchain=/usr --prefix=/usr 94 | ``` 95 | 96 | `path-to-swift` whould point exactly to your swift distribution. Pay attension that you have to put `/usr` after it. 97 | 98 | * Build: 99 | 100 | ```sh 101 | make 102 | ``` 103 | 104 | * Install (*Don't worry, it will NOT install system wide*) 105 | 106 | ```sh 107 | make install 108 | ``` 109 | 110 | # Next tutorial: [Hello Express](./helloexpress.md) -------------------------------------------------------------------------------- /doc/gettingstarted/routing.md: -------------------------------------------------------------------------------- 1 | # Basic Routing 2 | 3 | [Express](http://swiftexpress.io) routing definitions have the following pattern: 4 | 5 | ```swift 6 | app.METHOD(PATH, HANDLER) 7 | ``` 8 | 9 | `METHOD` can be one of the `get`, `post`, `put`, `delete`, `patch` or `all`. 10 | 11 | ##### Examples: 12 | 13 | Respond with `Hello World!` on the homepage: 14 | 15 | ```swift 16 | app.get("/") { request in 17 | return Action.ok("Hello World!") 18 | } 19 | ``` 20 | 21 | Respond to `POST` request on the root route `/`, the application’s home page: 22 | 23 | ```swift 24 | app.post("/") { request in 25 | return Action.ok("Got a POST request") 26 | } 27 | ``` 28 | 29 | Respond to a `PUT` request to the `/user` route: 30 | 31 | ```swift 32 | app.put("/user") { request in 33 | return Action.ok("Got a PUT request at /user") 34 | } 35 | ``` 36 | 37 | Respond to a `DELETE` request to the `/user` route: 38 | 39 | ```swift 40 | app.delete("/user") { request in 41 | return Action.ok("Got a DELETE request at /user") 42 | } 43 | ``` 44 | 45 | Respond to all methods requests on the `/user` route: 46 | 47 | ```swift 48 | app.all("/user") { request in 49 | return Action.ok("Got a " + request.method + " request at /user") 50 | } 51 | ``` 52 | 53 | 54 | # Next tutorial: [Static files](./static.md) -------------------------------------------------------------------------------- /doc/gettingstarted/static.md: -------------------------------------------------------------------------------- 1 | # Static files 2 | 3 | Static files are very easy to be served with [Express](http://swiftexpress.io): 4 | 5 | ```swift 6 | app.get("/:file+", action: StaticAction(path: "public", param:"file")) 7 | ``` 8 | 9 | The code above tells [Express](http://swiftexpress.io/) to serve all static files from the `public` folder recursively (i.e. it will serve both `public/article.html` as well as `public/articles/awesome.html`). If you want to serve just the first level in folder, use: 10 | 11 | ```swift 12 | app.get("/:file", action: StaticAction(path: "public", param:"file")) 13 | ``` 14 | 15 | The difference is just in the pattern: `/:file` versus `/:file+`. For more information see our [Advanced Routing](#) section. 16 | 17 | # Next tutorial: [Basic error handling](./errorhandling.md) -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # Express documentation 2 | 3 | * Getting started 4 | * [Installing](./gettingstarted/installing.md) 5 | * [Hello Express](./gettingstarted/helloexpress.md) 6 | * [Express Command Line](./gettingstarted/commandline.md) 7 | * [Building and running](./gettingstarted/buildrun.md) 8 | * [Basic routing](./gettingstarted/routing.md) 9 | * [Static files](./gettingstarted/static.md) 10 | * [Basic error handling](./gettingstarted/errorhandling.md) -------------------------------------------------------------------------------- /logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossroadlabs/Express/cc372cee22387f1064ae06386bb4ce986b394082/logo-full.png --------------------------------------------------------------------------------

    3 | 4 | Swift Express 5 | 6 |