build-lists: true theme: Letters from Sweden, 4
^ Thank When I Work
^ Using Swift outside of Xcode - Adam Mika
[.build-lists: false]
Frameworks available 1
^ Vapor catching up
[.build-lists: false]
- I read through the docs for both
- I joined both Slack groups
- Vapor felt more interesting to me ¯\_(ツ)_/¯
- Interesting article comparing the communities of Perfect and Vapor
^
^
^
^ plan to build with Perfect
[.build-lists: false]
-
Add Homebrew Tap
brew tap vapor/homebrew-tap brew update -
Install Vapor
brew install vapor -
Verify Installation
vapor --help
[.build-lists: false]
-
Create the App
vapor new SwiftMN cd SwiftMN -
Build the app
vapor build -
Run the app
.build/debug/App
Open a browser and navigate to localhost:8080
Make sure you change your scheme to App
Xcode
AppCode
^ play button in Xcode bug button in AppCode
[.build-lists: false]
They're all over the place and incomplete
docs.vapor.codesbeta.docs.vapor.codes- docs.vapor.codes/2.0/
- api.vapor.codes
- read the code github.com.vapor/vapor
- ask on Slack
^ read the docs, then code, then ask on Slack ^ active on Slack
Droplet()
The Droplet is a service container that gives you access to many of Vapor's facilities. It is responsible for registering routes, starting the server, appending middleware, and more.
^ The Droplet is everything. It's basically your entire app. It handles: routing, caching, environment variables... pretty much anything your app needs to do is done with the droplet.
import Vapor
let drop = try Droplet()
try drop.run()
^ This is all it takes to start your Vapor app.
^
^
^ 👀 Routing
import Vapor
let drop = try Droplet()
drop.get { req in
return try drop.view.make("welcome", [
"message": drop.localization[req.lang, "welcome", "title"]
])
}
try drop.run()
^ added a simple route
^
^
^ 👀 line by line
drop.get { req in
All GET requests to "/" will execute the block.
^ exposing route to "/" for GET
^
^ put, post, delete, etc.
drop.get { req in
return try drop.view.make("welcome", [
The Droplet will try to build and return welcome.leaf
^ drop.view.make builds a view
^ first parameter: welcome.leaf
^ leaf == templating engine
^ more later
^ 👀 view.make
public func make(_ path: String) throws -> View {
return try make(path, Node.null)
}
public func make(_ path: String, _ context: NodeRepresentable) throws -> View {
return try make(path, try context.makeNode())
}
public func make(_ path: String, _ context: [String: NodeRepresentable]) throws -> View {
return try make(path, try context.makeNode())
}
^ This is what view.make actually looks like
^ NodeRepresentable, helps with behind the scenes conversions
^ almost everything is a Node
drop.get { req in
return try drop.view.make("welcome", [
"message": drop.localization[req.lang, "welcome", "title"]
])
}
We pass a localized message in our context object
^
^ So what about URIs other than /
The verbs are Variadic functions
drop.get("about") { req in
// handle GET requests to "/about"
}
drop.post("some", "other", "place") { req in
// handle POST requests to /some/other/place
}
drop.get("anything", "*") { req in
// wildcards match anything after /anything
}
^ break 'em down
Path Parameters
drop.put("events", ":id") { req in
guard let id = req.parameters["id"]?.string else {
throw Abort.badRequest
}
// Update the event with the given id
}
^ specify path paramter
^ typical use case: id
^
^
^ 👀 But they take it even further than that
TypeSafe Path Parameters
drop.put("events", Int.self) { req, id in
// Update the event with the given id
}
^ expect int, second argument is id. else throw
drop.put("events", Event.self) { req, event in
// Update the given event
}
^
^ conform to Model protocol
^ next version adds Parameterizable protocol
^ Model is tightly coupled to their database protocols
^ defining simple routes in main.swift is fine, but that file will get out of control fast
^
^
^ 👀 So let's start moving things into controllers
drop.group("events") { route in
let eventController = MeetupController(drop: drop)
route.get(handler: eventController.listEvents)
route.get(":id", handler: eventController.getEvent)
}
All requests to /events will execute this closure
^ So instead of defining each request 1 at a time, we can group our endpoints together
^
^
^ 👀 line by line
drop.group("events") { route in
All requests to /events should execute this closure
^ all requests to the "/events" endpoint should execute this closure, GET, PUT, POST, DELETE /events /events/:id
drop.group("events") { route in
let eventController = MeetupController(drop: drop)
Create a Controller to handle each request
^ MeetupController instance to handle each request
drop.group("events") { route in
let eventController = MeetupController(drop: drop)
route.get(handler: eventController.listEvents)
route.get(":id", handler: eventController.getEvent)
}
GET requests to "/events" will hit the listEvents function
GET requests to "/events/:id" will hit the getEvent function
^ route.get is similar to drop.get, but with a handler parameter instead of a closure to execute
^
^
^ 👀 listEvents
MeetupController
func listEvents(_ request: Request) throws -> ResponseRepresentable {
let events: [Event] = try fetchEvents(request)
var upcoming: [Event] = []
var past: [Event] = []
events.forEach {
switch $0.status {
case .upcoming: upcoming.append($0)
case .past: past.append($0)
}
}
return try drop.view.make("listEvents", [
"allEvents": events.makeNode(),
"upcomingEvents": upcoming.makeNode(),
"pastEvents": past.makeNode()
])
}
^ First line: fetch from meetup API
^
^
^ 👀 fetchEvents
MeetupController
private func fetchEvents(_ request: Request) throws -> [Event] {
let path = "https://api.meetup.com/SwiftMN/events"
let headers: [HeaderKey: String] = [
HeaderKey.contentType: "application/json"
]
let query: [String: CustomStringConvertible] = [
"status": "upcoming,past",
"desc": true
]
// synchronous request to the meetup API
let eventsResponse = try drop.client.get(path, headers: headers, query: query)
... // a lot of parsing and data conversion happens
return events
}
^ define our path, headers, and query
^ drop.client.get is synchronous
^ async???
^
^
^ 👀 back to listEvents
MeetupController
func listEvents(_ request: Request) throws -> ResponseRepresentable {
let events: [Event] = try fetchEvents(request)
var upcoming: [Event] = []
var past: [Event] = []
events.forEach {
switch $0.status {
case .upcoming: upcoming.append($0)
case .past: past.append($0)
}
}
return try drop.view.make("listEvents", [
"allEvents": events.makeNode(),
"upcomingEvents": upcoming.makeNode(),
"pastEvents": past.makeNode()
])
}
^ fetch events, split into past and upcoming
^ build our view
^
^
^ 👀 which brings us to
^ leaf is Vapor's templating engine
^
^
^ 👀 And you define a leaf like this:
base.leaf
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/styles/app.css">
#import("head")
#embed("bootstrap")
</head>
<body>
#import("body")
</body>
</html>
^ basic HTML document
^ #imports the head and body from whatever #extends this base.leaf
^
^
^ 👀
listEvents.leaf
#extend("base")
#export("head") {
<title>SwiftMN Events</title>
}
#export("body") {
...
}
Inside our #export("body") { ... }
<div class=row>
#loop(upcomingEvents, "event") {
<div class="col-xs-6 col-lg-2">
#(event.formattedDate)
</div>
<div class="col-xs-6 col-lg-10">
<a href="/events/#(event.id)">#(event.name)</a></li>
</div>
}
</div>
^ twitter bootstrap here
^
^
^ 👀 take that away
#loop
#loop(upcomingEvents, "event") {
...
}
#loop iterates over the upcomingEvents from our context object and provides us with a variable named event
MeetupController.listEvents
A reminder of where upcomingEvents came from
return try drop.view.make("listEvents", [
"allEvents": events.makeNode(),
"upcomingEvents": upcoming.makeNode(),
"pastEvents": past.makeNode()
])
#(variable)
#(event.formattedDate)
Display the formattedDate variable on our event
<a href="/events/#(event.id)">#(event.name)</a></li>
Build an achor tag using event.id in the url and event.name as the link text
^
^
^ 👀 put it all back together
Inside our #export("body") { ... }
<div class=row>
#loop(upcomingEvents, "event") {
<div class="col-xs-6 col-lg-2">
#(event.formattedDate)
</div>
<div class="col-xs-6 col-lg-10">
<a href="/events/#(event.id)">#(event.name)</a></li>
</div>
}
</div>
^ So we have a div defining our row.
^ any number events with a date and a url to the event page
^
^
^ 👀
[fit] vapor.swift.mn
[.build-lists: false]
-
build on top of an existing leaf
#extend("base") -
import code from an extended leaf
#import("template") -
export code to the leaf that you've extended
#export("template") { <a href="#()"></a> } -
embed another document
#embed("commonCSS")
-
variables
#(event.name) -
literal "#" character
#() -
equality checking
#equal(thisVar, thatVar) { thisVar and thatVar are equal 👏 }
-
if / else if / else
#if(entering) { Hello, there! } ##if(leaving) { Goodbye! } ##else() { I've been here the whole time. }
-
iterate over an array
#loop(friends, "friend") { <li>#(friend.name)</li> } -
grab a single item out of an array using it's index
#index(events, 0) -
grab a single item out of a Dictionary
#index(friends, "best")
-
render as html/css/js instead of as a leaf document
#raw() { <a href="#raw">Anything goes!@#$%^&*</a> } -
render an html string stored in a variable
#raw(event.description)
^ Be careful rendering html from an API
^
^ Luckily, I write these descriptions so I'm not too worried about blindly rendering them.
class Index: BasicTag {
let name = "index"
func run(arguments: [Argument]) throws -> Node? {
guard
arguments.count == 2,
let array = arguments[0].value?.nodeArray,
let index = arguments[1].value?.int,
index < array.count
else { return nil }
return array[index]
}
}
^ actual code for #index
^ To build your own, just conform to the BasicTag protocol
main.swift
After conforming to the BasicTag protocol, register your tag in main.swift
if let leaf = drop.view as? LeafRenderer {
leaf.stem.register(Index())
}
^
^
^
^
^ 👀 back to handling requests
^ alter the request alter the response security early exit request logging
Middleware
public protocol Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Response
}
^ key: chaining
AuthMiddleware
func respond(to request: Request, chainingTo next: Responder) throws -> Response {
guard let bearer = request.auth.header?.bearer?.string else {
throw Abort.custom(status: .unauthorized, message: "Not Authorized")
}
// throws if not authenticated
let token = try validateToken(bearer)
request.storage["token"] = token
return try next.respond(to: request)
}
^ request.storage
extension Request {
func token() throws -> AuthToken {
guard let token = request.storage["token"] as? AuthToken else {
throw notAuthorizedError
}
return token
}
}
// anywhere after AuthMiddleware
let token = try request.token()
main.swift
let secure = drop.grouped(AuthMiddleware())
secure.group("user") { route in
// everything here is secured
}
EtagMiddleware
func respond(to request: Request, chainingTo next: Responder) throws -> Response {
if checkEtag(request.headers[.ifNoneMatch]) {
return Response(status: .notModified)
}
return try next.respond(to: request)
}
^ custom convenience initializer
main.swift
let secure = drop.grouped(AuthMiddleware(), EtagMiddleware())
secure.group("user") { route in
// everything here is secured
}
^ We're going to use the 2.0 instructions because they're easier
-
You'll need curl
apt install curl -
Easily add Vapor's APT repo with this handy script
eval "$(curl -sL https://apt.vapor.sh)" -
Install Swift and Vapor
sudo apt-get install swift vapor -
Double check the installation was successful
eval "$(curl -sL check2.vapor.sh)"
-
clone the repo
cd /home/ubuntu git clone https://github.com/vlaminck/SwiftMN.git cd SwiftMN -
Update Swift Packages
swift package update -
build for release
vapor build --release
[.build-lists: false]
-
Run your app manually to verify that it works
.build/release/App --env=production
[.build-lists: false]
- Open a browser and navigate to your server
Your app as a system service
sudo mv etc/app.service /lib/systemd/system
sudo chown root:root /lib/systemd/system/app.service
sudo systemctl daemon-reload
systemctl status app
Auto reloading
sudo systemctl enable app
sudo systemctl restart app
Auto Deployments
// TODO:
^ I started looking Jenkins and Docker, but I ran out of time
^ Another interesting option is Flock
- 3rd party options
[.build-lists: false]
- Available frameworks
- Getting started with vapor
- Routing
- Controllers / meetup API
- Leaf (templating engine)
- vapor.swift.mn
- Manual Deployments
[.build-lists: false]
- More meetup API integration
- talk suggestions
- 👍
- Anything you want
^ do what you think is interesting ^ Vapor, Perfect, RxSwift 👍
WWDC Micro Talks
^ anything you find interesting
[fit] slack.swift.mn
^ #talks, #wwdc
Footnotes
-
Github Stars as of 2017/5/15 ↩



