The Joy Of Server Side Swift Development

Preview:

Citation preview

The Joy Of Server Side Swift Development

In 1943 Konrad Zuse invented Plankalkül

1940s

9 Programming languages

1950s

... - Fortran(concept) - Fortran II - ALGOL 58(IAL) - LISP(implementation) ...

1950s

58 Programming languages

1960s

...- ALGOL 60 - COBOL 61(implementation) - FORTRAN IV - APL(concept) - Simula(concept) - SNOBOL - CPL - SNOBOL3 - ALGOL 68(concept) - BASIC ...

1960s

113 Programming languages

1970s

... - Pascal - Smalltalk - C - Prolog - Structured Query language ( SQL) - Bourne Shell(sh) - Modula-2 - AWK...

1970s

170 Programming languages

1980s

... - Ada 80(MIL-STD-1815) - GW-BASIC - Turbo Pascal - Objective-C - C++ - Eiffel - Erlang...

1980s

231 Programming languages

1990s

... - AMOS BASIC - Haskell - Python - Visual Basic - Brainfuck - Borland Delphi - Java - PHP - Ruby - JavaScript - Standard C++...

1990s

292 Programming languages

2000s

...- ActionScript- C#- D- Scala- Groovy- F#- Clojure- Go...

2000s

339 Programming languages

2010s

... - Rust - Elm - Kotlin - Elixir - Hack - C++14 ...

2010s

354 Programming languages

and then...

355

Do we really need another programming language?

Who Am I?@giordanoscalzohttps://github.com/gscalzo

A developer

After one year

For Real!

Officially, only Linux is Supported

Being Open-Source...

Server side development without a web framework?

We have a few...

Perfect: github.github.com/PerfectlySoft/Perfect (8829) Vapor: github.com/vapor/vapor (6763) Kitura: github.com/IBM-Swift/Kitura (4578) Swifton: github.com/necolt/Swifton (2016) Zewo: github.com/Zewo/Zewo (1358) Blackfish: github.com/elliottminns/blackfish (932) Slimane: github.com/noppoMan/Slimane (61) Tailor: github.com/brownleej/tailor (55) Kunugi: github.com/novi/Kunugi (35) Quark: github.com/QuarkX/Quark (33)

"Perfect is a complete and powerful toolbox,

framework, and application server for Linux, iOS, and

macOS"

They got money(Even without React. js!)

Open-source-ish, but enterprise-ish...

Sean StephensChief Executive Officer

let server = HTTPServer()

var routes = Routes()routes.add(method: .get, uri: "/", handler: { request, response in response.setHeader(.contentType, value: "text/html") response.appendBody(string: "<html><title>Hello, world!</title><body>Hello, world!</body></html>") response.completed() })

server.addRoutes(routes)server.serverPort = 8181server.documentRoot = "./webroot"configureServer(server)

do { try server.start()} catch PerfectError.networkError(let err, let msg) { print("Network error thrown: \(err) \(msg)")}

4 Laravel inspired

4 Easy to setup

4 Fantastic Command Line Tool

let drop = Droplet()drop.get("/") { request in return try drop.view.make("welcome.html")}drop.get("plaintext") { request in return "Hello, World!"}drop.serve()

4 Backed by IBM

4 Express.js-like

4 Perfectly integrated in IBM cloud solution BlueMix

import Kitura

let router = Router()

router.get("/") { request, response, next in response.send("Hello, World!") next()}

Kitura.addHTTPServer(onPort: 8080, with: router)Kitura.run()

Let's talk now of...

Performance!

Benchmarks are difficult

and useless...

Swift Frameworks Benchmarks

Fair Enough

Let's do some code

You can write Basic in Swift...

You can write Haskell-ish in Swift...

The sense of Swift

An Isomorphic Swift App!

A Universal Swift App!

Introducing

Mr Drizzle!

An hyper local weather app

MrDrizzle Server Requirements:4 Convert from Wunderground Models to MrDrizzle

Model

4 Refresh cache every hour

MrDrizzle Server

mkdir MrDrizzleServercd MrDrizzleServerswift package init --type executable

Swift Package Managerimport PackageDescription

let package = Package( name: "MrDrizzleServer", dependencies: [ .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 0) ])

Let's make it run

// main.swiftimport Foundationimport HTTPimport Vapor

let wheaterUndergroundKey = "KEY"let wheaterUndergroundDomain = "api.wunderground.com"

let drop = Droplet()

drop.get("/hello") { _ in return "Hello World"}

drop.run()

$ swift build

> Cloning https://github.com/vapor/vapor.git> HEAD is now at 453f7cf Merge pull request #628> Resolved version: 1.0.3> Cloning https://github.com/vapor/crypto.git> HEAD is now at 0aaa68b Merge pull request #15> Resolved version: 1.0.1> Cloning https://github.com/vapor/core.git........> Compile Swift Module 'TypeSafeRouting' (3 sources)> Compile Swift Module 'Vapor' (83 sources)> Compile Swift Module 'MrDrizzleServer' (1 sources)> Linking ./.build/debug/MrDrizzleServer

$ .build/debug/MrDrizzleServer

> Could not load localization files> No cipher key was set, using blank key.> Chacha20 cipher requires an initialization > No command supplied, defaulting to serve> No preparations.> No servers.json configuration found.> Starting server at 0.0.0.0:8080

$ swift package generate-xcodeproj> generated: ./MrDrizzleServer.xcodeproj

Hello World for dummies

This version:

drop.get("/hello") { _ in return "Hello World"}

Is equivalent to:

func handler(request: Request) throws -> ResponseRepresentable { return Response(status: .ok, body: "Hello, World")}drop.get("/hello", handler: handler)

because:

extension Swift.String: ResponseRepresentable { public func makeResponse() -> Response { return Response(body: self.bytes) }}

Go back to MrDrizzleServer

The list of the stations:

struct StationInfo { let name: String let stationId: String let lat: Float let lon: Float}

let stationsInfo = [ StationInfo(name: "Chelsea", stationId: "ILONDON330", lat: 51.479633, lon:-0.180277), StationInfo(name: "Westminster", stationId: "ILONDON120", lat: 51.65343, lon:-0.183732) //...]

Let's define a schema for our models

Model schema in pseudocode

object Stations { Array of Station stations }object Station { string id float lat float lon string name Array of HourlyMeasure measures}object HourlyMeasure { int32 hour float temp_f float temp_c}

syntax = "proto3";

message Stations { repeated Station stations = 1;}message Station { string id = 1; float lat = 2; float lon = 3; string name = 4; repeated HourlyMeasure measures = 5;}message HourlyMeasure { int32 hour = 1; float temp_f = 2; float temp_c = 3;}

protobuf

Invented by Apple

Given this:

syntax = "proto3";

message Stations { repeated Station stations = 1;}message Station { string id = 1; float lat = 2; float lon = 3; string name = 4; repeated HourlyMeasure measures = 5;}message HourlyMeasure { int32 hour = 1; float temp_f = 2; float temp_c = 3;}

/* * DO NOT EDIT. * * Generated by the protocol buffer compiler. * Source: mrdrizzle.proto * */

public struct Stations: ProtobufGeneratedMessage { public var swiftClassName: String {return "Stations"} public var protoMessageName: String {return "Stations"} public var protoPackageName: String {return ""} //...}

public struct Station: ProtobufGeneratedMessage { public var swiftClassName: String {return "Station"} public var protoMessageName: String {return "Station"} public var protoPackageName: String {return ""} //...}

public struct HourlyMeasure: ProtobufGeneratedMessage { public var swiftClassName: String {return "HourlyMeasure"} public var protoMessageName: String {return "HourlyMeasure"} public var protoPackageName: String {return ""}

//...}

We extend these structs to instantiate from the wunderground JSON

Wheater Underground JSON:

{ "response": { "version": "0.1", }, "hourly_forecast": [ { "FCTTIME": { "hour": "22", "mday": "19", "mday_padded": "19", "yday": "292", "isdst": "1", "age": "", "UTCDATE": "" }, "temp": { "english": "52", "metric": "11" }, "condition": "Clear", "icon": "clear", "metric": "-9999" }, ...

extension Station { init?(stationInfo: StationInfo, json: JSON) { guard let hourly_forecast = json["hourly_forecast"]?.array as? [JSON] else { return nil }

let measures = hourly_forecast.flatMap { HourlyMeasure(json: $0) }

self.init(lat: stationInfo.lat, lon: stationInfo.lon, name: stationInfo.name, measures: measures) }}

extension HourlyMeasure { init?(json: JSON) { guard let fctTime = json["FCTTIME"]?.object, let hour = fctTime["hour"]?.string, let temp = json["temp"]?.object, let tempF = temp["english"]?.string, let tempC = temp["metric"]?.string else { return nil }

self.init(hour: Int32(hour), tempF: Float(tempF), tempC: Float(tempC)) }}

Retrieving from Wunderground Server

extension Station { init?(stationInfo: StationInfo, client: ClientProtocol.Type) { let url = "http://\(wheaterUndergroundDomain)/api/" + "\(wheaterUndergroundKey)/hourly/q/pws:" + "\(stationInfo.stationId).json" guard let response = try? client.get(url), case .data(let bytes) = response.body, let json = try? JSON(bytes: bytes) else { return nil }

self.init(stationInfo: stationInfo, json: json) } }

Refreshing every hour

We cannot use URLSession :-(class WeatherService { private let refreshQueue = DispatchQueue(label: "com.mrdrizzle.refresh") private(set) var stations = Stations()

init(stationsInfo: [StationInfo], client: ClientProtocol.Type) { refresh(stationsInfo: stationsInfo, client: client) }

private func refresh(stationsInfo: [StationInfo], client: ClientProtocol.Type) { stations = stations.refreshed(given: stationsInfo, client: client) refreshQueue.asyncAfter(deadline: .now() + 3600) { [weak self] in self?.refresh(stationsInfo: stationsInfo, client: client) } } }

private extension Stations { func refreshed(given stationsInfo: [StationInfo], client: ClientProtocol.Type) -> Stations { return Stations(stations: stationsInfo.flatMap { Station(stationInfo: $0, client: client) }) }}

Putting everything together

//main.swiftlet drop = Droplet()

let stationsInfo = [ StationInfo(name: "Chelsea", stationId: "ILONDON330", lat: 51.479633, lon:-0.180277), StationInfo(name: "Westminster", stationId: "ILONDON120", lat: 51.65343, lon:-0.183732) //...]

let weatherService = WeatherService(stationsInfo: stationsInfo, client: drop.client)

drop.get("/api/weather") { _ in let stations = weatherService.stations

guard let data = try? stations.serializeProtobuf(), let bytes = try? data.makeBytes() else { throw Abort.serverError }

return Response(status: .ok, body: .data(bytes))}

drop.run()

Job done!

MrDrizzle Client

Networking classes from

https://talk.objc.io/episodes/S01E01-

networking

struct Resource<A> { let url: URL let parse: (Data) -> A?}

for example:

let url = URL(string: "http://mrdrizzle.com:8080/hello")!let helloResource = Resource<String>(url: url, parse: { data in return String(data: data, encoding: .utf8)})

class ApiService { func load<A>(resource: Resource<A>, completion: @escaping (A?) -> ()) { URLSession.shared.dataTask(with: resource.url) { data, response, error in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else { completion(nil) return }

completion(resource.parse(data)) }.resume() }}

let url = URL(string: "http://mrdrizzle.com:8080/hello")!let helloResource = Resource<String>(url: url) { data in return String(data: data, encoding: .utf8)}

ApiService().load(resource: helloResource) { guard let hello = $0 else { print("ERROR - Hello") return }

print(hello)}

let url = URL(string: "http://mrdrizzle.com:8080/hello")!let stationResources = Resource<Stations>(url: url) { data in return try? Stations(protobuf: data)}

ApiService().load(resource: stationResources) { guard let stations = $0 else { print("ERROR - Stations") return }

print(stations)}

It's ok :-)

Recap

However...

Apps are so 2014!

Let's do something modern

A Deep Learning Functional Reactive Chatbot!

Story behind it

Diagram of a Facebook Messenger ChatBot

Chat Flow

Let's do it

import PackageDescription

let package = Package( name: "UncleLucio", dependencies: [ .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 0) ])

// main.swiftimport Foundationimport Vaporimport HTTP

let PAGE_ACCESS_TOKEN = "STUFF"let fbURL = "https://graph.facebook.com/v2.6/me/messages?access_token=" + PAGE_ACCESS_TOKEN

let uncleLucio = UncleLucio(jokesDB: JokesDB())let drop = Droplet()

// main.swiftdrop.get("fbwebhook") { request in guard let token = request.data["hub.verify_token"]?.string, let response = request.data["hub.challenge"]?.string else { throw Abort.badRequest }

if token == "UNCLELUCIO_VERIFICATION_TOKEN" { return response // Response(status: .ok, text: response) } else { return "Error, invalid token"//Response(status: .ok, text: "Error, invalid token") }}

// main.swiftdrop.post("fbwebhook") { request in

request.log()

guard let jsonBody = request.json, let chatMessage = ChatMessage(message: jsonBody) else { throw Abort.badRequest }

let replyMessage = uncleLucio.message(after: chatMessage) return try drop.client.post(fbURL, headers: ["Content-Type": "application/json; charset=utf-8"], body: replyMessage.toJSON())}

drop.run()

// ChatMessage.swiftimport Foundationimport Vaporimport HTTP

struct ChatMessage { let sender: String let recipient: String let text: String?}

Payload send by Facebook callback

{ "object": "page", "entry": [ { "id": "1677732245875632", "time": 1476209402183, "messaging": [ { "sender": { "id": "1243544059050238" }, "recipient": { "id": "1677732245875632" }, "timestamp": 1476209402090, "message": { "mid": "mid.1476209402075:82aff934133d72d746", "seq": 17, "text": "Knock knock" } } ] } ]}

// ChatMessage.swiftextension ChatMessage { init?(message: JSON) { guard let entry = message["entry"]?.array?.first as? JSON else { return nil }

guard let messaging = entry["messaging"]?.array?.first as? JSON else { return nil }

guard let senderWrapper = messaging["sender"]?.object, let sender = senderWrapper["id"]?.string else { return nil } self.sender = sender guard let recipientWrapper = messaging["recipient"]?.object, let recipient = recipientWrapper["id"]?.string else { return nil } self.recipient = recipient guard let message = messaging["message"]?.object else { return nil } self.text = message["text"]?.string }}

// ChatMessage.swiftextension ChatMessage { func toJSON() throws -> JSON { return try JSON(node: [ "sender": try JSON(node: [ "id": sender ]), "recipient": try JSON(node: [ "id": recipient ]), "message": try JSON(node: [ "text": text ]), ]) }}

// UncleLucio.swiftclass UncleLucio { private let jokesDB: JokesDB private var sessions = [String: State]()

init(jokesDB: JokesDB) { self.jokesDB = jokesDB }

func message(after chatMessage: ChatMessage) -> ChatMessage { //... return replyMessage }}

// UncleLucio.swift func message(after chatMessage: ChatMessage) -> ChatMessage { let state = sessions[chatMessage.sender] ?? StartState(joke: jokesDB.randomJoke())

let (text, newState) = state.nextState(when: chatMessage.text ?? "pota")

if newState is Done { sessions.removeValue(forKey: chatMessage.sender) } else { sessions[chatMessage.sender] = newState } let replyMessage = ChatMessage(sender: chatMessage.recipient, recipient: chatMessage.sender, text: text) return replyMessage }

// UncleLucio.swift

protocol State { func nextState(when message: String) -> (String, State)}

// UncleLucio.swift

struct StartState: State { let joke: Joke func nextState(when message: String) -> (String, State) { if message.lowercased().contains("joke") { return ("Knock Knock", WaitingForReplyState(joke: joke)) } return ("pota", self) }}

struct WaitingForReplyState: State { let joke: Joke func nextState(when message: String) -> (String, State) { let text = message.lowercased() if text.contains("who's there") || text.contains("who is there") { return ("\(joke.subject)!", WaitingForSubjectReplyState(joke: joke)) } return ("pota", StartState(joke: joke)) }}

struct WaitingForSubjectReplyState: State { let joke: Joke func nextState(when message: String) -> (String, State) { let text = message.lowercased() if text.contains("\(joke.subject.lowercased()) who") { return ("\(joke.punchline)\nahahah", Done()) } return ("pota", StartState(joke: joke)) }}

struct Done: State { func nextState(when message: String) -> (String, State) { return ("pota", Done()) }}

Really easy to test

func testInStart_ReceivingGarbage_SaysSlogan_GoesStart() { let state = StartState(joke: joke())

let (text, nextState) = state.nextState(when: "foobar") XCTAssertEqual(text, "pota")

XCTAssertTrue(nextState is StartState) }

func testWaitingForSubjectReply_ReceivingReply_SaysPunchline_GoesDone() { let state = WaitingForSubjectReplyState(joke: joke())

let (text, nextState) = state.nextState(when: "\(joke().subject) who") XCTAssertEqual(text, "\(joke().punchline)\nahahah") XCTAssertTrue(nextState is Done) }

class JokesDB { private let jokes: [Joke] = [ ("Harry", "Harry up and let me in!"), ("Wanda", "Wanda hang out with me right now?"), ("Olive", "Olive you and I don’t care who knows it!"), ("Ho-ho", "You know, your Santa impression could use a little work."), ("Hanna", "...Hanna partridge in a pear tree!"), ("Mary and Abbey", "Mary Christmas and Abbey New Year!"), ("Irish", "Irish you a Merry Christmas!"), ("Yule log", "Yule log the door after you let me in, won’t you?"), ("Ya", "I’m excited to see you too!"), ("Sherlock", "Sherlock your door shut tight!"), ("Scold", "Scold outside—let me in!"), ("Robin", "Robin you! Hand over your cash!"), ("Needle", "Needle little help gettin’ in the door."), ("Nana", "Nana your business who’s there."), ("Luke", "Luke through the keyhole to see!"), ("Isabelle", "Isabelle working, or should I keep knocking?"), ].map { Joke(subject: $0.0, punchline: $0.1) }

func randomJoke() -> Joke { return jokes.randomItem() }}

extension Array { func randomItem() -> Element { let index = Int(arc4random_uniform(UInt32(self.count))) return self[index] }}

https://github.com/gscalzo/UncleLucio

Finally...

A reason for using Swift in Server...

Often in our job...

But sometimes...

"Everything in Its Right Place" (cit.)

Thank You!

Recommended