Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type safe routing #133

Merged
merged 15 commits into from
Mar 16, 2016
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ If you are having trouble connecting, make sure your ports are open. Check out `

## Routing

Routing in Vapor is simple and very similar to Laravel.
Routing in Vapor is simple and expressive.

`main.swift`
```swift
Expand All @@ -60,27 +60,49 @@ app.get("welcome") { request in
}
```

Here we will respond to all HTTP GET requests to `http://example.com/welcome` with the string `"Hello"`.
Here we will respond to all GET requests to `http://example.com/welcome` with the string `"Hello"`.

### JSON

Responding with JSON is easy.

```swift
app.get("version") { request in
return ["version": "1.0"]
return Json(["version": "1.0"])
}
```

This responds to all HTTP GET requests to `http://example.com/version` with the JSON dictionary `{"version": "1.0"}` and `Content-Type: application/json`.
This responds to all GET requests to `http://example.com/version` with the JSON dictionary `{"version": "1.0"}` and `Content-Type: application/json`.

### Type Safe Routing

Vapor supports [Frank](https://github.com/nestproject/Frank) inspired type-safe routing.

```swift
app.get("users", Int, "posts", String, "comments") { request, userId, postName in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readme should have compilable code examples IMO..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think including .self will confuse people an detract from the point the documentation is trying to get across.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is trying to show how clever we are, and not how to use it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This excerpt from the swift mailing list makes me feel like .self is confusing for people

+1 for junking the .self requirement

On Wed, Mar 9, 2016 at 11:14 PM, David Hart via swift-evolution <swift-evolution@swift.org> wrote:
I strongly agree for the removal of .self. I remember it being a great source of confusion when I first learned Swift.


I had the same experience. It was very disorienting. 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its requirement was confusing. Thats still exactly the case here; we're just adding more confusion by providing code examples omitting it.

return "You requested the comments for user #\(userId)'s post named \(postName))"
}
```

Here we will respond to all GET requests to `http://example.com/users/<userId>/posts/<postName>/comments`

You can also extend your own types to conform to Vapor's `StringInitializable` protocol. Here is an example where the `User` class conforms.

```swift
app.get("users", User) { request, user in
return "Hello \(user.name)"
}
```

Now requesting a `User` is expressive and concise.

### Views

You can also respond with HTML pages.

```swift
app.get("/") { request in
return View(path: "index.html")
return try View(path: "index.html")
}
```

Expand Down
232 changes: 232 additions & 0 deletions Sources/Generator/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//
// main.swift
// Vapor
//
// Created by Tanner Nelson on 3/13/16.
// Copyright © 2016 Tanner Nelson. All rights reserved.
//

import Foundation

let GENERIC_MAP = ["T", "U", "V", "W", "X"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like its sensible to have at least one more parameter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it 5 from 7 and 6 because they took significantly longer to compile than 5 in general development. Maybe a couple other people could run it w/ 6 and see how the compile times feel

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 5 is more than enough. Check out the caveat section in VaporWiki.Routing for my reasoning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason for 6 is it fits 3 nested components. i.e.:

blogs/:blog_id/posts/:post_id/comments/:comment_id

Which is needed for things like DELETE on a comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true... @loganwright how much longer did it take to compile?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have specifics, w/ 7 especially, it took long enough that I was worried Xcode was broken. I think you guys should try locally. The logic for 6 is pretty strong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always add more. Starting with less is fine I think.

let MAX_PARAMS = GENERIC_MAP.count

struct Func: CustomStringConvertible {
enum Method {
case Get, Post, Put, Patch, Delete, Options
}

var method: Method
var params: [Param]

var description: String {



let wildcards = params.filter { param in
return param.type == .Wildcard
}


var f = ""
f += "\tpublic func "
f += "\(method)".lowercaseString

//generic <>
if wildcards.count > 0 {
let genericsString = wildcards.map { wildcard in
return "\(wildcard.generic): StringInitializable"
}.joinWithSeparator(", ")

f += "<\(genericsString)>"
}

let paramsString = params.enumerate().map { (index, param) in
if index > 0 {
return "_ \(param)"
} else {
return param.description
}
}.joinWithSeparator(", ")

f += "(\(paramsString), handler: (Request"

//handler params
if wildcards.count > 0 {
let genericsString = wildcards.map { wildcard in
return wildcard.generic
}.joinWithSeparator(", ")

f += ", \(genericsString)) throws -> ResponseConvertible) {\n"

} else {
f += ") throws -> ResponseConvertible) {\n"
}

let pathString = params.map { param in
if param.type == .Wildcard {
return ":\(param.name)"
}

return "\\(\(param.name))"
}.joinWithSeparator("/")

f += "\t\tself.add(.\(method), path: \"\(pathString)\") { request in\n"

//function body
if wildcards.count > 0 {
//grab from request params
for wildcard in wildcards {
f += "\t\t\tguard let v\(wildcard.name) = request.parameters[\"\(wildcard.name)\"] else {\n"
f += "\t\t\t\tthrow Abort.BadRequest\n"
f += "\t\t\t}\n"
}

f += "\n"

//try
for wildcard in wildcards {
f += "\t\t\tlet e\(wildcard.name) = try \(wildcard.generic)(from: v\(wildcard.name))\n"
}

f += "\n"

//ensure conversion worked
for wildcard in wildcards {
f += "\t\t\tguard let c\(wildcard.name) = e\(wildcard.name) else {\n"
f += "\t\t\t\tthrow Abort.BadRequest\n"
f += "\t\t\t}\n"
}

f += "\n"


let wildcardString = wildcards.map { wildcard in
return "c\(wildcard.name)"
}.joinWithSeparator(", ")


f += "\t\t\treturn try handler(request, \(wildcardString))\n"

} else {

f += "\t\t\treturn try handler(request)\n"
}

f += "\t\t}\n"

f += "\t}"
return f
}
}

func paramTypeCount(type: Param.`Type`, params: [Param]) -> Int {
var i = 0

for param in params {
if param.type == type {
i += 1
}
}

return i
}

struct Param: CustomStringConvertible {
var name: String
var type: Type
var generic: String

var description: String {
var description = "\(name): "
if type == .Wildcard {
description += "\(generic).Type"
} else {
description += "String"
}
return description
}

enum `Type` {
case Path, Wildcard
}
static var types: [Type] = [.Path, .Wildcard]

static func addTypePermutations(toArray paramsArray: [[Param]]) -> [[Param]] {
var permParamsArray: [[Param]] = []

for paramArray in paramsArray {
for type in Param.types {
var mutableParamArray = paramArray

var name = ""
if type == .Wildcard {
name = "w"
} else {
name = "p"
}

let count = paramTypeCount(type, params: paramArray)
name += "\(count)"

let generic = GENERIC_MAP[count]

let param = Param(name: name, type: type, generic: generic)

mutableParamArray.append(param)
permParamsArray.append(mutableParamArray)
}
}

return permParamsArray
}
}




var paramPermutations: [[Param]] = []

for paramCount in 0...MAX_PARAMS {
var perms: [[Param]] = [[]]
for _ in 0..<paramCount {
perms = Param.addTypePermutations(toArray: perms)
}

paramPermutations += perms
}

var generated = "// *** GENERATED CODE ***\n"
generated += "// \(NSDate())\n"
generated += "//\n"
generated += "// DO NOT EDIT THIS FILE OR CHANGES WILL BE OVERWRITTEN\n\n"
generated += "extension Application {\n\n"

for method: Func.Method in [.Get, .Post, .Put, .Patch, .Delete, .Options] {
for params in paramPermutations {
guard params.count > 0 else {
continue
}

var f = Func(method: method, params: params)
generated += "\(f)\n\n"
}
}

generated += "}\n"

if Process.arguments.count < 2 {
fatalError("Please pass $SRCROOT as a parameter")
}

let path = Process.arguments[1].stringByReplacingOccurrencesOfString("XcodeProject", withString: "")
let url = NSURL(fileURLWithPath: path + "/Sources/Vapor/Core/Generated.swift")

do{
// writing to disk
try generated.writeToURL(url, atomically: true, encoding: NSUTF8StringEncoding)
print("File created at \(url)")
} catch let error as NSError {
print("Error writing generated file at \(url)")
print(error.localizedDescription)
}