├── .gitignore ├── .swift-version ├── CHANGELOG.md ├── Generate ├── Template.swift └── main.swift ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Application.swift ├── Convenience.swift ├── Generated.swift ├── ParameterConvertible.swift ├── ParameterParser.swift ├── Response.swift ├── ResponseConvertible.swift └── Route.swift ├── Tests ├── FrankTests │ ├── TestApplication.swift │ ├── TestParameterConvertible.swift │ ├── TestParameterParser.swift │ └── Tests.swift └── LinuxMain.swift ├── docs ├── Makefile ├── _templates │ └── sidebar_intro.html ├── conf.py ├── index.rst └── requirements.txt └── example └── example.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /Packages/ 3 | 4 | example/example 5 | Generate/generator 6 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Frank Changelog 2 | 3 | ## 0.4.0 4 | 5 | Adds support for Swift 3.1. Swift < 3 is no longer supported. 6 | -------------------------------------------------------------------------------- /Generate/Template.swift: -------------------------------------------------------------------------------- 1 | /// NOTE: This file is generated for type-safe path routing 2 | 3 | import Nest 4 | 5 | 6 | // The following line is a hack to make it possible to pass "*" as a Parameter 7 | // This makes it possible to annotate the place in the path that is variable 8 | public typealias Parameter = (Int, Int) -> Int 9 | 10 | 11 | func validateParameter(parser: ParameterParser, _ value: String) -> String? { 12 | if let parameter = parser.shift() where parameter == value { 13 | return parameter 14 | } 15 | 16 | return nil 17 | } 18 | 19 | 20 | extension Application { 21 | {% for method in methods %} 22 | /// {{ method }} / 23 | func {{ method|lowercase }}(closure: RequestType -> ResponseConvertible) { 24 | route("{{ method }}") { request in 25 | if request.path == "/" { 26 | return { closure(request) } 27 | } 28 | 29 | return nil 30 | } 31 | } 32 | 33 | {% for parameters in combinations %}{% hasVariables %} 34 | /// {{ method }} 35 | func {{ method|lowercase }}{% if hasVariables %}<{% for variable in variables %}P{{ variable }} : ParameterConvertible{% ifnot forloop.last %}, {% endif %}{% endfor %}>{% endif %}({% for variable in parameters %}{% ifnot forloop.first %}_ {% endif %}p{{ forloop.counter }}: {% if variable %}Parameter{% else %}String{% endif %}, {% endfor %} _ closure: (RequestType{% for variable in parameters %}{% if variable %}, P{{ forloop.counter }}{% endif %}{% endfor %}) -> ResponseConvertible) { 36 | route("{{ method }}") { request in 37 | let parser = ParameterParser(path: request.path) 38 | 39 | if {% for variable in parameters %} 40 | let {% if variable %}p{{ forloop.counter }} = P{{ forloop.counter }}(parser: parser){% else %}_ = validateParameter(parser, p{{ forloop.counter }}){% endif %}{% ifnot forloop.last %},{% endif %}{% endfor %} 41 | where parser.isEmpty 42 | { 43 | return { 44 | closure(request{% for variable in parameters %}{% if variable %}, p{{ forloop.counter }}{% endif %}{% endfor %}) 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | } 51 | {% endfor %} 52 | {% endfor %} 53 | } 54 | 55 | {% for method in methods %} 56 | /// {{ method }} / 57 | public func {{ method|lowercase }}(closure: (RequestType) -> ResponseConvertible) { 58 | application.{{ method|lowercase }}(closure) 59 | } 60 | 61 | {% for parameters in combinations %}{% hasVariables %} 62 | /// {{ method }} 63 | public func {{ method|lowercase }}{% if hasVariables %}<{% for variable in variables %}P{{ variable }} : ParameterConvertible{% ifnot forloop.last %}, {% endif %}{% endfor %}>{% endif %}({% for variable in parameters %}{% ifnot forloop.first %}_ {% endif %}p{{ forloop.counter }}: {% if variable %}Parameter{% else %}String{% endif %}, {% endfor %} _ closure: (RequestType{% for variable in parameters %}{% if variable %}, P{{ forloop.counter }}{% endif %}{% endfor %}) -> ResponseConvertible) { 64 | application.{{ method|lowercase }}({% for parameter in parameters %}p{{ forloop.counter }}, {% endfor %}closure) 65 | } 66 | {% endfor %}{% endfor %} 67 | -------------------------------------------------------------------------------- /Generate/main.swift: -------------------------------------------------------------------------------- 1 | import PathKit 2 | import Stencil 3 | 4 | 5 | /// Every HTTP method to generate a function for 6 | let methods = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"] 7 | 8 | 9 | func permute(count: Int, items: [[Bool]] = []) -> [[Bool]] { 10 | if count == 0 { 11 | return items 12 | } 13 | 14 | if let item = items.last where item.count == count { 15 | return items 16 | } 17 | 18 | let lastItem = items.last ?? [] 19 | 20 | return ( 21 | permute(count, items: items + [lastItem + [true]]) + 22 | permute(count, items: [lastItem + [false]]) 23 | ) 24 | } 25 | 26 | 27 | /// Returns all the possible combinations of functions variable combinations 28 | func combinations() -> [[Bool]] { 29 | return permute(5) 30 | } 31 | 32 | 33 | do { 34 | let namespace = Namespace() 35 | namespace.registerSimpleTag("hasVariables") { context in 36 | if let parameters = context["parameters"] as? [Bool] { 37 | context["hasVariables"] = parameters.contains { $0 } 38 | context["variables"] = parameters.reduce([(Bool,Int)]()) { accumulator, isVariable in 39 | let count = accumulator.count + 1 40 | return accumulator + [(isVariable, count)] 41 | }.filter { isVariable, _ in isVariable }.map { _, count in count } 42 | } 43 | 44 | return "" 45 | } 46 | 47 | let templatePath = Path(__FILE__) + "../Template.swift" 48 | let template = try Template(path: templatePath) 49 | let context = Context(dictionary: [ 50 | "methods": methods, 51 | "combinations": combinations(), 52 | ]) 53 | 54 | let result = try template.render(context, namespace: namespace) 55 | let destination = Path(__FILE__) + "../../Sources/Generated.swift" 56 | 57 | try destination.write(result) 58 | } catch { 59 | print("Failed to generate: \(error)") 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Kyle Fuller 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SWIFTC=swiftc 2 | 3 | ifeq ($(shell uname -s), Darwin) 4 | XCODE=$(shell xcode-select -p) 5 | SDK=$(XCODE)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk 6 | TARGET=x86_64-apple-macosx10.10 7 | SWIFTC=swiftc -target $(TARGET) -sdk $(SDK) -Xlinker -all_load 8 | endif 9 | 10 | LIBS=Frank Curassow Commander Inquiline Nest 11 | TEST_LIBS=$(LIBS) Spectre 12 | GENERATE_LIBS=$(LIBS) PathKit Stencil 13 | 14 | SWIFT_ARGS=$(foreach lib,$(LIBS),-Xlinker .build/debug/$(lib).a) 15 | TEST_SWIFT_ARGS=$(foreach lib,$(TEST_LIBS),-Xlinker .build/debug/$(lib).a) 16 | GENERATE_SWIFT_ARGS=$(foreach lib,$(GENERATE_LIBS),-Xlinker .build/debug/$(lib).a) 17 | 18 | frank: 19 | @echo "Building Frank" 20 | @swift build 21 | 22 | test: frank 23 | @.build/debug/spectre-build 24 | 25 | example: frank 26 | @$(SWIFTC) -o example/example \ 27 | example/example.swift \ 28 | -I.build/debug \ 29 | $(SWIFT_ARGS) 30 | 31 | generator: frank 32 | @$(SWIFTC) -o Generate/generator \ 33 | Generate/main.swift \ 34 | -I.build/debug \ 35 | $(GENERATE_SWIFT_ARGS) 36 | 37 | generate: generator 38 | Generate/generator 39 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | 4 | let package = Package( 5 | name: "Frank", 6 | dependencies: [ 7 | .Package(url: "https://github.com/nestproject/Nest.git", majorVersion: 0, minor: 4), 8 | .Package(url: "https://github.com/nestproject/Inquiline.git", majorVersion: 0, minor: 4), 9 | .Package(url: "https://github.com/kylef/Curassow.git", majorVersion: 0, minor: 6), 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frank 2 | 3 | Frank is a DSL for quickly writing web applications in Swift with type-safe 4 | path routing. 5 | 6 | ##### `Sources/main.swift` 7 | 8 | ```swift 9 | import Frank 10 | 11 | // Handle GET requests to path / 12 | get { request in 13 | return "Hello World" 14 | } 15 | 16 | // Handle GET requests to path /users/{username} 17 | get("users", *) { (request, username: String) in 18 | return "Hello \(username)" 19 | } 20 | ``` 21 | 22 | ##### `Package.swift` 23 | 24 | ```swift 25 | import PackageDescription 26 | 27 | let package = Package( 28 | name: "Hello", 29 | dependencies: [ 30 | .Package(url: "https://github.com/kylef/Frank.git", majorVersion: 0, minor: 4) 31 | ] 32 | ) 33 | ``` 34 | 35 | Then build, and run it via: 36 | 37 | ```shell 38 | $ swift build --configuration release 39 | $ .build/release/Hello 40 | [2016-01-25 07:13:21 +0000] [25678] [INFO] Listening at http://0.0.0.0:8000 (25678) 41 | [2016-01-25 07:13:21 +0000] [25679] [INFO] Booting worker process with pid: 25679 42 | ``` 43 | 44 | Check out the [full example](https://github.com/nestproject/Frank-example) 45 | which can be deployed to Heroku. 46 | 47 | ### Usage 48 | 49 | #### Routes 50 | 51 | Routes are constructed with passing your path split by slashes `/` as 52 | separate arguments to the HTTP method (e.g. `get`, `post` etc) functions. 53 | 54 | For example, to match a path of `/users/kyle/followers` you can use the following: 55 | 56 | ```swift 57 | get("users", "kyle", "followers") { request in 58 | 59 | } 60 | ``` 61 | 62 | You may pass path components along with wildcard (`*`) to match variables in 63 | paths. The wildcard is a placemarker to annotate where the variable path 64 | components are in your path. Frank allows you to use any number of wildcards 65 | in any place of the path, allowing you to match all paths. 66 | 67 | The wildcards will map directly to parameters in the path and the variables 68 | passed into your callback. Wildcard parameters are translated to the type 69 | specified in your closure. 70 | 71 | ```swift 72 | // /users/{username} 73 | get("users", *) { (request, username: String) in 74 | return "Hi \(username)" 75 | } 76 | 77 | // /users/{username}/followers 78 | get("users", *, "followers") { (request, username: String) in 79 | return "\(username) has 5 followers" 80 | } 81 | ``` 82 | 83 | You may place any type that conforms to `ParameterConvertible` 84 | in your callback, this allows the types to be correctly 85 | converted to your type or user will face a 404 since the 86 | URL will be invalid. 87 | 88 | ```swift 89 | // /users/{userid} 90 | get("users", *) { (request, userid: Int) in 91 | return "Hi user with ID: \(userid)" 92 | } 93 | ``` 94 | 95 | ##### Custom Parameter Types 96 | 97 | Wildcard parameters may be of any type that conforms to `ParameterConvertible`, 98 | this allows you to match against custom types providing you conform to 99 | `ParameterConvertible`. 100 | 101 | For example, we can create a Status enum which can be Open or Closed which 102 | conforms to `ParameterConvertible`: 103 | 104 | ```swift 105 | enum Status : ParameterConvertible { 106 | case open 107 | case closed 108 | 109 | init?(parser: ParameterParser) { 110 | switch parser.shift() ?? "" { 111 | case "open": 112 | self = .open 113 | case "closed": 114 | self = .closed 115 | default: 116 | return nil 117 | } 118 | } 119 | } 120 | 121 | get("issues", *) { (request, status: Status) in 122 | return "Issues using status: \(status)" 123 | } 124 | ``` 125 | 126 | ##### Adding routes 127 | 128 | Routes are matched in the order they are defined. The first route that matches 129 | the request is invoked. 130 | 131 | ```swift 132 | get { 133 | ... 134 | } 135 | 136 | put { 137 | ... 138 | } 139 | 140 | patch { 141 | ... 142 | } 143 | 144 | delete { 145 | ... 146 | } 147 | 148 | head { 149 | ... 150 | } 151 | 152 | options { 153 | ... 154 | } 155 | ``` 156 | 157 | #### Return Values 158 | 159 | The return value of route blocks takes a type that conforms to the 160 | `ResponseConvertible` protocol, which means you can make any type Response 161 | Convertible. For example, you can return a simple string: 162 | 163 | ```swift 164 | get { 165 | return "Hello World" 166 | } 167 | ``` 168 | 169 | Return a full response: 170 | 171 | ```swift 172 | get { 173 | return Response(.ok, headers: ["Custom-Header": "value"]) 174 | } 175 | 176 | post { 177 | return Response(.created, content: "User created") 178 | } 179 | ``` 180 | 181 | #### Templates 182 | 183 | ##### Stencil 184 | 185 | You can easily use the [Stencil](https://github.com/kylef/Stencil) template 186 | language with Frank. For example, you can create a convenience function to 187 | render templates (called `stencil`): 188 | 189 | ```swift 190 | import Stencil 191 | import Inquiline 192 | import PathKit 193 | 194 | 195 | func stencil(path: String, _ context: [String: Any]? = nil) -> ResponseConvertible { 196 | do { 197 | let template = try Template(path: Path(path)) 198 | let body = try template.render(Context(dictionary: context)) 199 | return Response(.ok, headers: [("Content-Type", "text/html")], content: body) 200 | } catch { 201 | return Response(.internalServerError) 202 | } 203 | } 204 | ``` 205 | 206 | Which can easily be called from your route to render a template: 207 | 208 | ```swift 209 | get { 210 | return stencil("hello.html", ["user": "world"]) 211 | } 212 | ``` 213 | 214 | ###### `hello.swift` 215 | 216 | ```html+django 217 | 218 | 219 | Hello {{ user }}! 220 | 221 | 222 | ``` 223 | 224 | ### Nest 225 | 226 | Frank is design around the [Nest](https://github.com/nestproject/Nest) Swift 227 | Web Server Gateway Interface, which allows you to use any Nest-compatible web 228 | servers. The exposed `call` function is a Nest compatible application which can 229 | be passed to a server of your choice. 230 | 231 | ```swift 232 | import Frank 233 | 234 | get { 235 | return "Custom Server" 236 | } 237 | 238 | // Pass "call" to your HTTP server 239 | serve(call) 240 | ``` 241 | -------------------------------------------------------------------------------- /Sources/Application.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | import Inquiline 3 | 4 | 5 | class Application { 6 | var routes: [Route] = [] 7 | 8 | func call(_ request: RequestType) -> ResponseType { 9 | let routes = self.routes.flatMap { route -> (String, Route.Handler)? in 10 | if let match = route.match(request) { 11 | return (route.method, match) 12 | } 13 | 14 | return nil 15 | } 16 | 17 | let route = routes.filter { method, _ in method == request.method }.first 18 | 19 | if let (_, handler) = route { 20 | return handler().asResponse() 21 | } 22 | 23 | if !routes.isEmpty { 24 | return Response(.methodNotAllowed) 25 | } 26 | 27 | return Response(.notFound) 28 | } 29 | 30 | /// Register a route for the given method and path 31 | func route(_ method: String, _ path: String, _ closure: @escaping (RequestType) -> ResponseConvertible) { 32 | route(method) { request in 33 | if path == request.path { 34 | return { closure(request) } 35 | } 36 | 37 | return nil 38 | } 39 | } 40 | 41 | /// Register a route using a matcher closure 42 | func route(_ method: String, _ match: @escaping (RequestType) -> Route.Handler?) { 43 | let route = Route(method: method, match: match) 44 | routes.append(route) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Convenience.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin 5 | #endif 6 | 7 | import Nest 8 | import Curassow 9 | 10 | 11 | let application: Application = { 12 | atexit { serve() } 13 | return Application() 14 | }() 15 | 16 | 17 | func serve() -> Never { 18 | Curassow.serve(application.call) 19 | } 20 | 21 | 22 | public func call(_ request: RequestType) -> ResponseType { 23 | return application.call(request) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ParameterConvertible.swift: -------------------------------------------------------------------------------- 1 | public protocol ParameterConvertible { 2 | init?(parser: ParameterParser) 3 | } 4 | 5 | 6 | extension String : ParameterConvertible { 7 | public init?(parser: ParameterParser) { 8 | if let parameter = parser.shift() { 9 | self.init(parameter) 10 | } else { 11 | return nil 12 | } 13 | } 14 | } 15 | 16 | 17 | extension Int : ParameterConvertible { 18 | public init?(parser: ParameterParser) { 19 | if let parameter = parser.shift() { 20 | self.init(parameter) 21 | } else { 22 | return nil 23 | } 24 | } 25 | } 26 | 27 | 28 | extension Double : ParameterConvertible { 29 | public init?(parser: ParameterParser) { 30 | if let parameter = parser.shift() { 31 | self.init(parameter) 32 | } else { 33 | return nil 34 | } 35 | } 36 | } 37 | 38 | 39 | extension Float : ParameterConvertible { 40 | public init?(parser: ParameterParser) { 41 | if let parameter = parser.shift() { 42 | self.init(parameter) 43 | } else { 44 | return nil 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ParameterParser.swift: -------------------------------------------------------------------------------- 1 | public class ParameterParser { 2 | var components: [String] 3 | 4 | init(path: String) { 5 | components = path.characters.split(separator: "/").map(String.init) 6 | } 7 | 8 | public var isEmpty: Bool { 9 | return components.isEmpty 10 | } 11 | 12 | /// Returns and removes the next component from the path 13 | public func shift() -> String? { 14 | if isEmpty { 15 | return nil 16 | } 17 | 18 | return components.removeFirst() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Response.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | import Inquiline 3 | 4 | 5 | func redirect(location: String, status: Status = .found) -> ResponseType { 6 | var response = Response(status) 7 | response["Location"] = location 8 | return response 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ResponseConvertible.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | import Inquiline 3 | 4 | 5 | 6 | public protocol ResponseConvertible { 7 | func asResponse() -> ResponseType 8 | } 9 | 10 | 11 | extension String : ResponseConvertible { 12 | public func asResponse() -> ResponseType { 13 | return Response(.ok, content: self) 14 | } 15 | } 16 | 17 | 18 | extension Response : ResponseConvertible { 19 | public func asResponse() -> ResponseType { 20 | return self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Route.swift: -------------------------------------------------------------------------------- 1 | import Nest 2 | 3 | 4 | struct Route { 5 | /// The handler to perform the request 6 | typealias Handler = () -> ResponseConvertible 7 | 8 | /// The HTTP method to match the route 9 | let method: String 10 | 11 | /// The match handler 12 | let match: (RequestType) -> Handler? 13 | 14 | init(method: String, match: @escaping (RequestType) -> Handler?) { 15 | self.method = method 16 | self.match = match 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/FrankTests/TestApplication.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | import Nest 3 | import Inquiline 4 | @testable import Frank 5 | 6 | 7 | extension ResponseType { 8 | mutating func content() -> String? { 9 | if let body = body { 10 | var bytes: [UInt8] = [] 11 | var body = body 12 | while let nextBytes = body.next() { bytes += nextBytes } 13 | bytes.append(0) 14 | return String(cString: bytes.map { CChar($0) }) 15 | } 16 | 17 | return nil 18 | } 19 | } 20 | 21 | 22 | func testApplication() { 23 | describe("Application") { 24 | let app = Application() 25 | 26 | app.get { _ in 27 | return "Custom Route" 28 | } 29 | 30 | app.get("users", *) { (request, username: String) in 31 | return "Hello \(username)" 32 | } 33 | 34 | $0.it("returns my registered root route") { 35 | var response = app.call(Request(method: "GET", path: "/")) 36 | 37 | try expect(response.statusLine) == "200 OK" 38 | try expect(response.content()) == "Custom Route" 39 | } 40 | 41 | $0.it("returns my registered parameter route") { 42 | var response = app.call(Request(method: "GET", path: "/users/kyle")) 43 | 44 | try expect(response.statusLine) == "200 OK" 45 | try expect(response.content()) == "Hello kyle" 46 | } 47 | 48 | $0.it("returns a 405 when there is a route for the path, but not for the method") { 49 | let response = app.call(Request(method: "DELETE", path: "/")) 50 | 51 | try expect(response.statusLine) == "405 Method Not Allowed" 52 | } 53 | 54 | $0.it("returns a 404 for unmatched paths") { 55 | let response = app.call(Request(method: "GET", path: "/user/kyle")) 56 | 57 | try expect(response.statusLine) == "404 Not Found" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/FrankTests/TestParameterConvertible.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | @testable import Frank 3 | 4 | 5 | func testParameterConvertible() { 6 | describe("String is ParameterConvertible") { 7 | $0.it("matches with a strings") { 8 | let parser = ParameterParser(path: "/hello") 9 | try expect(String(parser: parser)) == "hello" 10 | } 11 | } 12 | 13 | describe("Int is ParameterConvertible") { 14 | $0.it("matches with an integer") { 15 | let parser = ParameterParser(path: "/123") 16 | try expect(Int(parser: parser)) == 123 17 | } 18 | 19 | $0.it("doesn't match non-integers") { 20 | let parser = ParameterParser(path: "/one") 21 | try expect(Int(parser: parser)).to.beNil() 22 | } 23 | } 24 | 25 | describe("Double is ParameterConvertible") { 26 | $0.it("matches with an integer") { 27 | let parser = ParameterParser(path: "/123") 28 | try expect(Double(parser: parser)) == 123 29 | } 30 | 31 | $0.it("matches with a decimal number") { 32 | let parser = ParameterParser(path: "/1.23") 33 | try expect(Double(parser: parser)) == 1.23 34 | } 35 | 36 | $0.it("doesn't match non-numbers") { 37 | let parser = ParameterParser(path: "/one") 38 | try expect(Double(parser: parser)).to.beNil() 39 | } 40 | } 41 | 42 | describe("Float is ParameterConvertible") { 43 | $0.it("matches with an integer") { 44 | let parser = ParameterParser(path: "/123") 45 | try expect(Float(parser: parser)) == 123 46 | } 47 | 48 | $0.it("matches with a decimal number") { 49 | let parser = ParameterParser(path: "/1.23") 50 | try expect(Float(parser: parser)) == 1.23 51 | } 52 | 53 | $0.it("doesn't match non-numbers") { 54 | let parser = ParameterParser(path: "/one") 55 | try expect(Float(parser: parser)).to.beNil() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/FrankTests/TestParameterParser.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | @testable import Frank 3 | 4 | 5 | func testParameterParser() { 6 | describe("ParameterParser") { 7 | $0.describe("isEmpty") { 8 | $0.it("returns when the path is empty") { 9 | let parser = ParameterParser(path: "/") 10 | try expect(parser.isEmpty).to.beTrue() 11 | } 12 | 13 | $0.it("returns when the path is not empty") { 14 | let parser = ParameterParser(path: "/not") 15 | try expect(parser.isEmpty).to.beFalse() 16 | } 17 | } 18 | 19 | $0.describe("shifting a path component") { 20 | $0.it("returns the component") { 21 | let parser = ParameterParser(path: "/path") 22 | try expect(parser.shift()) == "path" 23 | try expect(parser.isEmpty).to.beTrue() 24 | } 25 | 26 | $0.it("returns nil when there isn't a component") { 27 | let parser = ParameterParser(path: "/") 28 | try expect(parser.shift()).to.beNil() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/FrankTests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | 4 | public func test() { 5 | testApplication() 6 | testParameterParser() 7 | testParameterConvertible() 8 | } 9 | 10 | 11 | class FrankTests: XCTestCase { 12 | func testFrank() { 13 | test() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import FrankTests 2 | 3 | test() 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Curassow.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Curassow.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Curassow" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Curassow" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_templates/sidebar_intro.html: -------------------------------------------------------------------------------- 1 |

Frank

2 | 3 |

4 | 8 |

9 | 10 |

Frank is a micro web framework for Swift.

11 | 12 | 24 | 25 |

Other Projects

26 | 27 |

More Kyle Fuller projects:

28 | 36 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | 6 | from recommonmark.parser import CommonMarkParser 7 | 8 | # -- General configuration ------------------------------------------------ 9 | 10 | # If your documentation needs a minimal Sphinx version, state it here. 11 | #needs_sphinx = '1.0' 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [] 17 | 18 | # Add any paths that contain templates here, relative to this directory. 19 | templates_path = ['_templates'] 20 | 21 | source_parsers = { 22 | '.md': CommonMarkParser, 23 | } 24 | source_suffix = ['.rst', '.md'] 25 | 26 | # The encoding of source files. 27 | #source_encoding = 'utf-8-sig' 28 | 29 | # The master toctree document. 30 | master_doc = 'index' 31 | 32 | # General information about the project. 33 | project = u'Frank' 34 | copyright = u'2017, Kyle Fuller' 35 | author = u'Kyle Fuller' 36 | 37 | # The version info for the project you're documenting, acts as replacement for 38 | # |version| and |release|, also used in various other places throughout the 39 | # built documents. 40 | # 41 | # The short X.Y version. 42 | version = u'0.1' 43 | # The full version, including alpha/beta/rc tags. 44 | release = u'0.1.0' 45 | 46 | # The language for content autogenerated by Sphinx. Refer to documentation 47 | # for a list of supported languages. 48 | # 49 | # This is also used if you do content translation via gettext catalogs. 50 | # Usually you set "language" from the command line for these cases. 51 | language = None 52 | 53 | # There are two options for replacing |today|: either, you set today to some 54 | # non-false value, then it is used: 55 | #today = '' 56 | # Else, today_fmt is used as the format for a strftime call. 57 | #today_fmt = '%B %d, %Y' 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | exclude_patterns = ['_build'] 62 | 63 | # The reST default role (used for this markup: `text`) to use for all 64 | # documents. 65 | #default_role = None 66 | 67 | # If true, '()' will be appended to :func: etc. cross-reference text. 68 | #add_function_parentheses = True 69 | 70 | # If true, the current module name will be prepended to all description 71 | # unit titles (such as .. function::). 72 | #add_module_names = True 73 | 74 | # If true, sectionauthor and moduleauthor directives will be shown in the 75 | # output. They are ignored by default. 76 | #show_authors = False 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # A list of ignored prefixes for module index sorting. 82 | #modindex_common_prefix = [] 83 | 84 | # If true, keep warnings as "system message" paragraphs in the built documents. 85 | #keep_warnings = False 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | 91 | # -- Options for HTML output ---------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'alabaster' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (relative to this directory) to use as a favicon of 117 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # Add any extra paths that contain custom files (such as robots.txt or 127 | # .htaccess) here, relative to this directory. These files are copied 128 | # directly to the root of the documentation. 129 | #html_extra_path = [] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | html_sidebars = { 140 | 'index': ['sidebar_intro.html', 'searchbox.html'], 141 | '**': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 'searchbox.html'], 142 | } 143 | 144 | # Additional templates that should be rendered to pages, maps page names to 145 | # template names. 146 | #html_additional_pages = {} 147 | 148 | # If false, no module index is generated. 149 | #html_domain_indices = True 150 | 151 | # If false, no index is generated. 152 | #html_use_index = True 153 | 154 | # If true, the index is split into individual pages for each letter. 155 | #html_split_index = False 156 | 157 | html_show_sourcelink = True 158 | html_show_sphinx = False 159 | html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Language to be used for generating the HTML full-text search index. 170 | # Sphinx supports the following languages: 171 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 172 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 173 | #html_search_language = 'en' 174 | 175 | # A dictionary with options for the search language support, empty by default. 176 | # Now only 'ja' uses this config value 177 | #html_search_options = {'type': 'default'} 178 | 179 | # The name of a javascript file (relative to the configuration directory) that 180 | # implements a search results scorer. If empty, the default will be used. 181 | #html_search_scorer = 'scorer.js' 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'Frankdoc' 185 | 186 | # -- Options for LaTeX output --------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | #'papersize': 'letterpaper', 191 | 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #'preamble': '', 197 | 198 | # Latex figure (float) alignment 199 | #'figure_align': 'htbp', 200 | } 201 | 202 | # Grouping the document tree into LaTeX files. List of tuples 203 | # (source start file, target name, title, 204 | # author, documentclass [howto, manual, or own class]). 205 | latex_documents = [ 206 | (master_doc, 'Frank.tex', u'Frank Documentation', 207 | u'Kyle Fuller', 'manual'), 208 | ] 209 | 210 | # The name of an image file (relative to this directory) to place at the top of 211 | # the title page. 212 | #latex_logo = None 213 | 214 | # For "manual" documents, if this is true, then toplevel headings are parts, 215 | # not chapters. 216 | #latex_use_parts = False 217 | 218 | # If true, show page references after internal links. 219 | #latex_show_pagerefs = False 220 | 221 | # If true, show URL addresses after external links. 222 | #latex_show_urls = False 223 | 224 | # Documents to append as an appendix to all manuals. 225 | #latex_appendices = [] 226 | 227 | # If false, no module index is generated. 228 | #latex_domain_indices = True 229 | 230 | 231 | # -- Options for manual page output --------------------------------------- 232 | 233 | # One entry per manual page. List of tuples 234 | # (source start file, name, description, authors, manual section). 235 | man_pages = [ 236 | (master_doc, 'frank', u'Frank Documentation', 237 | [author], 1) 238 | ] 239 | 240 | # If true, show URL addresses after external links. 241 | #man_show_urls = False 242 | 243 | 244 | # -- Options for Texinfo output ------------------------------------------- 245 | 246 | # Grouping the document tree into Texinfo files. List of tuples 247 | # (source start file, target name, title, author, 248 | # dir menu entry, description, category) 249 | texinfo_documents = [ 250 | (master_doc, 'Frank', u'Frank Documentation', 251 | author, 'Frank', 'One line description of project.', 252 | 'Miscellaneous'), 253 | ] 254 | 255 | # Documents to append as an appendix to all manuals. 256 | #texinfo_appendices = [] 257 | 258 | # If false, no module index is generated. 259 | #texinfo_domain_indices = True 260 | 261 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 262 | #texinfo_show_urls = 'footnote' 263 | 264 | # If true, do not generate a @detailmenu in the "Top" node's menu. 265 | #texinfo_no_detailmenu = False 266 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Frank 2 | ===== 3 | 4 | Frank is a micro web framework for Swift. 5 | 6 | .. code-block:: swift 7 | 8 | import Frank 9 | 10 | // Handle GET requests to path / 11 | get { request in 12 | return "Hello World" 13 | } 14 | 15 | // Handle GET requests to path /users/{username} 16 | get("users", *) { (request, username: String) in 17 | return "Hello \(username)" 18 | } 19 | 20 | Quick Start 21 | ----------- 22 | 23 | To use Frank, you will need to install it via the Swift Package Manager, 24 | you can add it to the list of dependencies in your `Package.swift`: 25 | 26 | .. code-block:: swift 27 | 28 | import PackageDescription 29 | 30 | 31 | let package = Package( 32 | name: "HelloWorld", 33 | dependencies: [ 34 | .Package(url: "https://github.com/kylef/Frank.git", majorVersion: 0), 35 | ] 36 | ) 37 | 38 | Afterwards you can place your web application implementation in `Sources` 39 | and add the runner inside `main.swift` which exposes a command line tool to 40 | run your web application: 41 | 42 | .. code-block:: swift 43 | 44 | import Frank 45 | 46 | get { request in 47 | return "Hello World" 48 | } 49 | 50 | Then build and run your application: 51 | 52 | .. code-block:: shell 53 | 54 | $ swift build --configuration release 55 | $ ./.build/release/HelloWorld 56 | 57 | Check out the `Hello World example `_ application. 58 | 59 | Contents 60 | -------- 61 | 62 | .. toctree:: 63 | :maxdepth: 2 64 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.10 2 | appdirs==1.4.3 3 | argh==0.26.2 4 | Babel==2.4.0 5 | CommonMark==0.5.4 6 | docutils==0.13.1 7 | imagesize==0.7.1 8 | Jinja2==2.9.6 9 | livereload==2.5.1 10 | MarkupSafe==1.0 11 | packaging==16.8 12 | pathtools==0.1.2 13 | port-for==0.3.1 14 | Pygments==2.2.0 15 | pyparsing==2.2.0 16 | pytz==2017.2 17 | PyYAML==3.12 18 | recommonmark==0.4.0 19 | requests==2.14.2 20 | six==1.10.0 21 | snowballstemmer==1.2.1 22 | Sphinx==1.6.1 23 | sphinx-autobuild==0.6.0 24 | sphinxcontrib-websupport==1.0.1 25 | tornado==4.5.1 26 | typing==3.6.1 27 | watchdog==0.8.3 28 | -------------------------------------------------------------------------------- /example/example.swift: -------------------------------------------------------------------------------- 1 | import Frank 2 | 3 | get("/") { _ in 4 | return "Hello World" 5 | } 6 | --------------------------------------------------------------------------------