├── .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 \`make4 | 8 |
9 | 10 |Frank is a micro web framework for Swift.
11 | 12 | 24 | 25 |More Kyle Fuller projects:
28 |