Web Services with Swift
Total Page:16
File Type:pdf, Size:1020Kb
Steve Goodrich Server Side Swift With Vapor https://github.com/zepedebo/PwnedWatcher Server Side Swift With Vapor https://github.com/zepedebo/PwnedWatcher The Project: Build a database of email addresses and check them against haveibeenpwned.com https://github.com/zepedebo/PwnedWatcher The Technology https://github.com/zepedebo/PwnedWatcher Swift https://swift.org/ Swift Provides Modern approachable language Reasonably good tools Cross platform development Swift Playgrounds https://swift.org/ Vapor https://vapor.codes/ Vapor Provides: Vapor https://vapor.codes/ HTTP/HTTPS Endpoint Web Sockets Middleware Crypto Authentication Validation Session Multipart Data Management Routing Data Serialization Caching Templates Database Logging Async / Deserialization SQLite3 https://www.sqlite.org SQLite Provides: Ubiquitous Data Store SQLite3 Flexible https://www.sqlite.org No setup required Requirements (for macOS) Homebrew Xcode 9.3 or greater (https://brew.sh/) Check Your Xcode slc-steveg-orm:~ steveg$ xcode-select -p /Applications/Xcode-beta.app/Contents/Developer slc-steveg-orm:~ steveg$ sudo xcode-select -s /Applications/Xcode.app Install Vapor slc-steveg-orm:~ steveg$ brew install vapor/tap/vapor https://github.com/zepedebo/PwnedWatcher Pwned Watcher • Create a list of e-mail addresses • Check them against “';--have i been pwned?” • Display how many sites are a problem for me Demonstrates • Simple URL routing • Get and Post HTTP commands • MVC Architecture • Vapor Client calls to a RESTful API • Async Structure of a Vapor Project Generated with: vapor new --web WebHello Simplest Routing Structure of an Address The scheme The computer The application The service / api call https://haveibeenpwned.com:443/api/v2/breaches We are mainly concerned with this part Adding a route All routes start in routes.swift Simplest Route http://localhost/hello // routes.swift import Vapor public func routes(_ router: Router) throws { router. get("hello") { req in return "Hello" } } Managing Complexity Model View Controller Controllers are the Glue http://localhost/hello // routes.swift import Vapor public func routes(_ router: Router) throws { router. get("hello") { req in return "Hello" } } Model Add a Model with Fluent Fluent gives you • Support for: • An ORM • SQLite • Migrations • MySQL • Relations • PostgreSQL • Transactions Interesting Files package.swift - Which libraries are needed configure.swift - Configure services Models folder - Define our data models Add Package Information import PackageDescription Download this let package = Package( name: "URLWatcher", dependencies: [ // � A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), // Load the driver for sqlite3 .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"), // � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ .target(name: "App", dependencies: ["Leaf", "Vapor", "FluentSQLite"]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] ) Compile this Add a Class in Models import FluentSQLite import Vapor final class MonitoredEMail: SQLiteModel { var id: Int? var address: String init(id: Int? = nil, address: String) { self.id = id; self.address = address; } } extension MonitoredEMail: Content {} extension MonitoredEMail: Migration {} Add Configuration import Leaf import Vapor import FluentSQLite /// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { : try services.register(FluentSQLiteProvider()) : var databases = DatabasesConfig() try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite) services.register(databases) : var migrations = MigrationConfig() migrations.add(model: MonitoredEMail.self, database: .sqlite) services.register(migrations) } Change Package Information import PackageDescription let package = Package( name: "URLWatcher", dependencies: [ // � A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), // Load the driver for sqlite3 .package(url: "https://github.com/vapor/fluent-postgresql.git""https://github.com/vapor/fluent-sqlite.git", from:, from: "3.0.0" "1.0.0"), ) // � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ ..targettarget(name:(name: "App""App",, dependencies:dependencies: [["Leaf""Leaf",, "Vapor""Vapor",, "FluentPostgreSQL""FluentSQLite"]), ]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] ) Change the Class in Models import FluentSQLiteFluentPostgreSQL import Vapor final class MonitoredEMail: SQLiteModelPostgreSQLModel { { var id: Int? var address: String init(id: Int? = nil, address: String) { self.id = id; self.address = address; } } extension MonitoredEMail: Content {} extension MonitoredEMail: Migration {} Change Configuration import Leaf import Vapor import FluentSQLite /// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { : trytry services.services.registerregister((FluentPostgreSQLProviderFluentSQLiteProvider()) ()) : var databases = DatabasesConfig() trylet databases.pgconfig =add PostgreSQLDatabaseConfig(database: SQLiteDatabase(hostname:(storage: "localhost" .memory), ,as: port: .sqlite 32768), username: "postgres", services. register (databases) database: "email", password: nil, transport: .cleartext) let postgres = PostgreSQLDatabase(config: pgconfig) databases.add(database: postgres, as: .psql) services.register(databases) : var migrations = MigrationConfig() migrations.migrations.addadd(model:(model: MonitoredEMailMonitoredEMail..selfself,, database:database: ..sqlitepsql) ) services.register(migrations) } Model Model View Controller View Add a View with Leaf Add a View Add Package Information (package.swift) import PackageDescription let package = Package( name: "WebHello", Download this dependencies: [ // � A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), // � An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), ], targets: [ .target(name: "App", dependencies: ["Leaf", "Vapor"]), .target(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: ["App"]) ] Compile this ) Add Configuration (configure.swift) import Leaf import Vapor /// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { // Register providers first try services.register(LeafProvider()) : : : Add a .leaf File (email.leaf) #set("title") { Monitored Emails } #set("body") { <h1 class="mt-3"> Monitored E-Mail addresses </h1> <form method="POST" action="/emails"> <div class="input-group"> <input type="text" name="address" class="form-control"> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="submit"> Create </button> </div> </div> </form> <table class="table"> <tr> <th scope="col">E-Mail Address</th> </tr> #for(email in addresses) { <tr><td>#(email.address)</td></tr> } </table> } #embed("base") Render the View (in our controller) import Vapor /// Register your application's routes here. public func routes(_ router: Router) throws { // "It works" page router.get("emails") { req -> Future<View> in let storedAddresses = MonitoredEMail.query(on: req).all() return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } } } Model View Controller Controller Add a Controller with Swift Add GET and POST Handlers (Email.swift) import Vapor final class EmailController { func getEMails(_ req: Request) throws -> Future<View> { let storedAddresses = MonitoredEMail.query(on: req).all() return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } } func addEMail(_ req: Request) throws -> Future<Response> { return try req.content.decode(MonitoredEMail.self).flatMap { url in return url.save(on: req).map { _ in return req.redirect(to: "/emails") } } } } Connect to Routes (Routes.swift) import Vapor /// Register your application's routes here. public func routes(_ router: Router) throws { // "It works" page router.get("emails") { req -> Future<View> in let storedAddresses = MonitoredEMail.query(on: req).all() return storedAddresses.flatMap { addresses in let data = ["addresses": addresses] return try req.view().render("email", data) } } } Connect to Routes (Routes.swift) import Vapor /// Register your application's routes here. public func routes(_ router: Router) throws { let emailController = EmailController() router.get("emails", use:emailController.getEMails) router.post("emails", use:emailController.addEMail) Closures I do not think it runs when you think it runs Quick Closure Review add1 = lambda a: a + 1 let add1 = a => a + 1 let add1 = {(a: Int) -> Int in return a + 1} let add1 = {a in return a + 1} let add1 = {$0 + 1} These are VARIABLES. Trailing Closures [1,2,3].reduce(0, {$0 + $1}) == [1,2,3].reduce(0){$0+$1} [1,2,3].map{$0 * $0} Spot the Closures import Vapor Here final class EmailController { func getEMails(_ req: