(Taylor Swift on Rails. “Mean” Performance)

Swifton

A Ruby on Rails inspired Web Framework for Swift that runs on Linux and OS X.









Getting Started

  • Install latest Development snapshot from Swift.org or via swiftenv. If you are on OSX I highly recommend swiftenv – latest Swift will be able to coexist with system wide Swift that comes with Xcode.
  • swift --version should show something like: Swift version 3.0 - dev...
  • Checkout TodoApp example project.
  • Run swift build inside app (most of dependencies throw deprecation warnings).
  • Run ./.build/debug/Swifton-TodoApp.
  • Open http://0.0.0.0:8000/todos in your browser.

Contributing

Contributions are more than welcome! The easiest way to start contributing to Swifton:

  • Setup TodoApp
  • Pick one issue from the issues list or propose enhancement.
  • You can find Swifton source code in Swifton-TodoApp/Packages/Swifton-<version> directory. Packages inside Packages directory comes with Git repository so feel free to do you changes there.
  • Compile and test TodoApp, this will help to check your changes and avoid regressions.
  • Commit and push your changes, open pull request.
  • Enjoy 😉

Routing

Swifton comes with ready to use Router, also you can use any router as long as it accepts Request and returns Response. Routes are defined in main.swift file. Configured Router is passed to Nest interface supporting server. Swifton Router supports RFC6570 URI Templates via URITemplate library. Router allows to define resources and regular routes.

.
let router = Router()
router.resources("todos", TodosController())
.Copy the code

Which is equivalent to:

let router = Router()
router.get("/todos/new", TodosController()["new"])
router.get("/todos/{id}", TodosController()["show"])
router.get("/todos/{id}/edit", TodosController()["edit"])
router.get("/todos", TodosController()["index"])
router.post("/todos", TodosController()["create"])
router.delete("/todos/{id}", TodosController()["destroy"])
router.patch("/todos/{id}", TodosController()["update"])Copy the code

Configured routes then are passed to application server.

.
serve { request in
    router.respond(request) 
}
.Copy the code

Controllers

A controller inherits from ApplicationController class, which inherits from Controller class. Action is a closure that accepts Request object and returns Response object. beforeAction and afterAction allows to register filters before and after action is executed.

class TodosController: ApplicationController { 
    // shared todo variable used to pass value between setTodo filter and actions
    var todo: Todo?    
    override init() { super.init()

    // sets before filter setTodo only for specified actions 
    beforeAction("setTodo"["only": ["show"."edit"."update"."destroy"]])

    // render all Todo instances with Index template (in Views/Todos/Index.html.stencil)
    action("index") { request in
        let todos = ["todos": Todo.allAttributes()]
        return self.render("Todos/Index", todos)
    }

    // render Todo instance that was set in before filter
    action("show") { request in
        return self.render("Todos/Show", self.todo)
    }

    // render static New template
    action("new") { request in
        return self.render("Todos/New")}// render Todo instance's edit form
    action("edit") { request in
        return self.render("Todos/Edit", self.todo)
    } 

    // create new Todo instance and redirect to list of Todos 
    action("create") { request in
        Todo.create(request.params)
        return self.redirectTo("/todos")}// update Todo instance and redirect to updated Todo instance
    action("update") { request inself.todo! .update(request.params)
        return self.redirectTo("/todos/\(self.todo! .id)")}// destroy Todo instance
    action("destroy") { request in
        Todo.destroy(self.todo)
        return self.redirectTo("/todos")}// set todo shared variable to actions can use it
    filter("setTodo") { request in
        // Redirect to "/todos" list if Todo instance is not found 
        guard let t = Todo.find(request.params["id"]) else { return self.redirectTo("/todos") } 
        self.todo = t as? Todo
        // Run next filter or action
        return self.next
    }

}}
Copy the code

respondTo allows to define multiple responders based client Accept header:

.
action("show") { request in
    return self.respondTo(request, [
        "html": { self.render("Todos/Show", self.todo) },
        "json": { self.renderJSON(self.todo) }
    ])
}
.
Copy the code

Models

Swifton is ORM agnostic web framework. You can use any ORM of your choice. Swifton comes with simple in-memory MemoryModel class that you can inherit and use for your apps. Simple as this:

class User: MemoryModel {
}

...

User.all.count // 0
var user = User.create(["name": "Saulius"."surname": "Grigaitis"])
User.all.count // 1
user["name"] / /"Saulius"
user["surname"] / /"Grigaitis"
user.update(["name": "James"."surname": "Bond"])
user["surname"] / /"Bond"
User.destroy(user)
User.all.count // 0
Copy the code

Few options if you need persistence:

  • PostgreSQL adapter.
  • MySQL adapter.
  • Fluent simple SQLite ORM.

Views

Swifton supports Mustache like templates via Stencil template language. View is rendered with controller’s method render(template_path, object). Object needs either to conform to HTMLRenderable protocol, either be [String: Any] type where Any allows to pass complex structures.

<tbody>
  {% for todo in todos %}
    <tr>
      <td>{{ todo.title }}</td>
      <td>{{ todo.completed }}</td>
      <td><a href="/todos/{{ todo.id }}">Show</a></td>
      <td><a href="/todos/{{ todo.id }}/edit">Edit</a></td>
      <td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/todos/{{ todo.id }}">Destroy</a></td>
    </tr>
  {% endfor %}
</tbody>
Copy the code

Views are loaded from Views directory by default, you can also change this default setting by changing value of SwiftonConfig.viewsDirectory (preferable in main.swift file). Currently views are not cached, so you don’t need to restart server or recompile after views are changed.

Static assets (JavaScript, CSS, images etc.) are loaded from Public directory by default, you can also change this default setting by changing value of SwiftonConfig.publicDirectory (preferable in main.swift file).

JSON support

renderJSON(object) generates and returns JSON of an object. Object must conform to JSONRenderable protocol.

action("show") { request in
    return self.respondTo(request, [
        "html": { self.render("Todos/Show".self.todo) },
        "json": { self.renderJSON(self.todo) }
    ])
}Copy the code

Middleware

main.swift is probably best place to put middleware. Simply wrap Router instance with your middleware, you can even nest multiple middlewares.

.
serve { request in
    router.respond(request) 
}
.Copy the code

Application Server

Swifton comes with Currasow server. Curassow is a Swift Nest HTTP Server. It uses the pre-fork worker model and it’s similar to Python’s Gunicorn and Ruby’s Unicorn. Swifton applications should run on other Nest servers with none or minimal modifications.

Curassow provides a command line interface to configure the address you want to listen on and the amount of workers you wish to use.

Building for production

Build release configuration for better performance:

$ swift build --configuration releaseCopy the code

Setting the workers

$ ./.build/release/Swifton-TodoApp --workers 4 
[arbiter] Listening on 0.0. 0. 0:8000
[arbiter] Started worker process 18405
[arbiter] Started worker process 18406
[arbiter] Started worker process 18407Copy the code

Configuring the address

$ ./.build/release/Swifton-TodoApp - the bind 127.0.0.1:9000
[arbiter] Listening on 127.0. 01.:9000Copy the code

Configuring worker timeouts

By default, Curassow will kill and restart workers after 30 seconds if it hasn’t prepared you for the master process.

$ ./.build/release/Swifton-TodoApp --timeout 30Copy the code

Deployment

Heroku

Example TodoApp can be deployed to Heroku using the heroku-buildpack-swift.

Click the button below to automatically set up this example to run on your own Heroku account.

Docker

Example TodoApp can be deployed on Docker using the docker-swift.