Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Vapor in the Wild

Vapor in the Wild

A showcase of the different use cases I've had using Vapor. This including the My Jenkins app which is an internal apps I have written for using our Jenkins CI boxes and adding new features like over-the-air distribution. It also covers the use of Vapor as the new backend for the CodeMobile UK conference as well as some smaller use cases. This talk was given at the CodeMobile 2019 conference.

Avatar for pdamonkey

pdamonkey

April 02, 2019

More Decks by pdamonkey

Other Decks in Programming

Transcript

  1. @pdamonkey Who am I? Matthew Gallagher iOS Chapter Lead (My

    Sky app team) Worked at Sky since 2015
  2. @pdamonkey What I’m going to cover? Why Vapor? Simple uses

    of Vapor Live Coding My Jenkins (internal Sky app) Code Mobile
  3. @pdamonkey Fast-forward to Vapor Liked the similarities to Laravel Played

    with Vapor when it was first released Couldn’t use advanced dynamic routing Couldn’t figure out pre-processors However, I found it hard to re-create the CMS
  4. @pdamonkey Vapor 3 Felt like the right time to take

    another look My Swift skills had improved, so less up-skilling Full re-write using Swift NIO Codable just keeps getting better More adoption, therefore more tutorials and help
  5. @pdamonkey What is My Jenkins? Easy distribution to other teams

    Provides over-the-air distribution Check status / trigger builds from anywhere Reduces Jenkins knowledge requirement Front-end for Jenkins CI boxes
  6. @pdamonkey Vapor to the rescue Jenkins API took a while

    to understand and use Tidy up the API Allow the API to be used by iOS and Android Retrieve project branches from GitHub Allow jobs to be filtered on the API
  7. @pdamonkey Task 1: Tidy API /api/json?tree=jobs[name,description,url,color,actions[ parameterDefinitions[description,name,type,choices, defaultParameterValue[name,value]]]healthReport[description, score],nextBuildNumber,queueItem[*],lastBuild[number,id, fullDisplayName,timestamp,duration,estimatedDuration,result,

    building,queueId,artifacts[*],actions[causes[userId,userName], parameters[*],lastBuiltRevision[SHA1]]],lastSuccessfulBuild[number, timestamp,duration],lastFailedBuild[number,timestamp,duration], scm[userRemoteConfigs[url]]] Jenkins API: ( characters) 495
  8. @pdamonkey Task 2: Clean the data init(from decoder: Decoder) throws

    { let container = try decoder.container(keyedBy: CodingKeys.self) number = try container.decode(Int.self, forKey: .number) id = try container.decode(String.self, forKey: .id) fullDisplayName = try container.decode(String.self, forKey: .fullDisplayName) building = try container.decode(Bool.self, forKey: .building) queueId = try container.decode(Int.self, forKey: .queueId) duration = try container.decode(Int.self, forKey: .duration) estimatedDuration = try container.decode(Int.self, forKey: .estimatedDuration) result = try container.decodeIfPresent(Result.self, forKey: .result) ?? .building timestamp = try container.decode(TimeInterval.self, forKey: .timestamp) let actions = try container.decodeIfPresent([Action].self, forKey: .actions) parameters = actions?.compactMap{ $0.parameters }.flatMap{ $0 } ?? [] let cause = actions?.compactMap{ $0.causes }.flatMap{ $0 }.first(where: { $0.type != .unsupported }) if let cause = cause { triggered = Triggered(id: cause.userID, name: cause.userName, by: String(describing: cause.type)) } else { triggered = Triggered(id: nil, name: nil, by: String(describing: Action.Cause.CauseType.unsupported)) } sha1 = actions?.compactMap{ $0.lastBuiltRevision?.SHA1 }.first consoleOutputURL = nil exportDetailsURL = nil cancelBuildURL = nil let artifacts = try container.decodeIfPresent([Artifact].self, forKey: .artifacts) self.artifacts = artifacts ?? [] hasExportDetails = (artifacts?.filter{ $0.fileName == "export.json" }.count ?? 0) > 0 }
  9. @pdamonkey Task 3: Different Endpoints enum Project: String, Parameter {

    case ios, android static func resolveParameter(_ parameter: String, on container: Container) throws -> Project { guard let project = Project(rawValue: parameter) else { throw Abort(.notFound) } return project } var displayName: String { switch self { case .ios: return "iOS" case .android: return "Android" } } var basePath: String { switch self { case .ios: return "https://jenkins-ios.sky.com" case .android: return "https://jenkins-android.sky.com" } } var headers: HTTPHeaders { switch self { case .ios: return ["Authorization": "Basic \(Environment.iOSJenkinsAuth)"] case .android: return ["Authorization": "Basic \(Environment.AndroidJenkinsAuth)"] } } }
  10. @pdamonkey Task 4: GitHub Access Allow multiple clients easy access

    to GitHub branches Keeping secure keys on the server What do you mean you need a proxy?
  11. @pdamonkey Proxies on Linux Proxies not fully supported in Swift

    on Linux Raised an issue on Vapor GitHub page: https://github.com/vapor/vapor/issues/1723 Thanks to support from community, found a workaround
  12. @pdamonkey #if os(macOS) headers.add(name: "Proxy-Authorization", value: "Basic 838fa8agjasdl=") sessionConfiguration.connectionProxyDictionary =

    [ "HTTPEnable": true, "HTTPPort": 8888, "HTTPProxy": 192.168.0.5, "HTTPSEnable": true, "HTTPSPort": 8888, "HTTPSProxy": 192.168.0.5 ] #else setenv("HTTP_PROXY", "http://username:[email protected]:8888", 1) setenv("HTTPS_PROXY", "https://username:[email protected]:8888", 1) #endif Workaround
  13. @pdamonkey Task 5: Filter Jobs enum Project: String, Parameter {

    ... var filteredJobs: [String] { switch self { case .ios: return ["App Store Build"] case .android: return [] } } } Allow non-required or sensitive jobs to be filtered out Less JSON returned
  14. @pdamonkey Open Source? If not will create my own open

    source version or articles showing how to create it Having discussions about how we would open source
  15. @pdamonkey How this started Spoke to Steve last year after

    Tim Condon’s Vapor talk Wouldn’t it be great to use Vapor as a back-end The answer was obviously YES!
  16. @pdamonkey Why a new back end? 
 More flexible security

    using GitHub OAuth 2 
 Wanted to make Steve’s life a lot easier ! Feature requests for the app would require new API features first Updates where previously a manual affair, no CMS
  17. @pdamonkey My Vapor Challenges Secure login Making a Dashboard Image

    uploads Apply DRY principal to stop repeated code
  18. @pdamonkey api.group("locations") { group in group.listView(page: .locations, adminItems: itemsRequest, sortedBy:

    [\Location.description]) group.addView("add", of: Location.self, page: .locations, adminItems: itemsRequest) group.editView(Location.parameter, of: Location.self, page: .locations, adminItems: itemsRequest) group.updateItem(Location.parameter, of: Location.self, page: .locations) group.deleteItem(Location.parameter, "delete", of: Location.self, page: .locations) }
  19. @pdamonkey #set("body") { <form method="post" class="needs-validation" novalidate> #embed("admin/#(pageDetails.area)/_form") <a href="/admin/#(pageDetails.area)"

    class="btn btn-secondary”>Cancel</a> &#160; <button type="submit" class="btn btn-primary”> Save changes </button> </form> }
  20. @pdamonkey @discardableResult func listView<Item, Path>( _ path: PathComponentsRepresentable..., of type:

    Item.Type = Item.self, page: Page, adminItems: ItemsRequest, sortedBy sortKeyPaths: [KeyPath<Item, Path>]) -> Route<Responder> where Item: Content & SQLiteModel { return get(path) { req throws -> Future<View> in var items = Item.query(on: req) for sortItem in sortKeyPaths { items = items.sort(sortItem) } let context = ListRequest( pageDetails: PageDetails(page: page, style: .list), items: items.all(), adminItems: adminItems ) return try req.privateContainer .make(ViewRenderer.self) .render("admin/template/list", context) } }
  21. @pdamonkey import Vapor import Leaf public func routes(_ router: Router)

    throws { router.get { req -> Future<View> in return try req.view().render("home") } router.post("convert-to-base64") { req -> Future<View> in struct Form: Content { let textToConvert: String? let convertedText: String? } return try req.content.decode(Form.self).flatMap(to: View.self) { form in let text = form.textToConvert ?? "" let convertedText = text.toBase64() let converted = Form(textToConvert: text, convertedText: convertedText) return try req.view().render("home", converted) } } } extension String { func toBase64() -> String { return Data(self.utf8).base64EncodedString() } }
  22. @pdamonkey <form method="POST" action="convert-to-base64"> <div class="form-group"> <label for="textToConvert" style="color: #838383;

    font-weight: bold;”> Enter plain text: </label> <textarea name="textToConvert" class="form-control" id="textToConvert" rows="6">#(textToConvert)</textarea> </div> <div class="form-group"> <label for="convertedText" style="color: #B4B4B4; font-weight: bold;”> Converted base 64 string: </label> <textarea class="form-control copiable-text" id="convertedText" rows="6" disabled>#(convertedText)</textarea> </div> <input name="convertToBase64" type="hidden" value="true"/> <button type="submit" class="btn btn-lg btn-primary">Convert</button> <button class="btn btn-lg btn-secondary" onclick="copy(); return false;”> <img src=“/assets/images/icon_copy.png"/> </button> </form>
  23. @pdamonkey Convert into Swift Using Scanner to scan the path

    Quick way to convert SVG path into Swift
  24. @pdamonkey while scanner.scanCharacters(from: instructionSet, into: &instruction) { var x =

    0.0, y = 0.0 var points: [CGPoint] = [] while scanner.scanDouble(&x) && scanner.scanDouble(&y) { points.append(CGPoint(x: x, y: y)) } switch instruction { case "M": output += "path.move(to: \(points[0].string))\n" currentPoint = points[0] case "m": let offsetPoint = currentPoint.offset(points[0]) output += "path.move(to: \(offsetPoint.string))\n" currentPoint = offsetPoint default: print("instruction is not supported”) }
  25. @pdamonkey What is OAuth 2? Started reading up and following

    this really useful guide: https://www.oauth.com/oauth2-servers/accessing-data/ Converted from PHP into Swift and Vapor Changed it into Middleware
  26. @pdamonkey App Store Reviews Using a post by Paul Samuels

    (@paulio87) as inspiration: https://paul-samuels.com/blog/2019/01/12/writing- custom-tools-with-swift/
  27. @pdamonkey vapor new app-store-reviews --template=web Name Flag Description API --template=api

    JSON API with Fluent database Web --template=web HTML website with Leaf templates Auth --template=auth-template JSON API with Fluent DB and Auth Creating Vapor Project
  28. @pdamonkey ** **~~** **~~~~~~** **~~~~~~~~~~** **~~~~~~~~~~~~~~** **~~~~~~~~~~~~~~~~~~** **~~~~~~~~~~~~~~~~~~~~~~** **~~~~~~~~~~~~~~~~~~~~~~~~** **~~~~~~~~~~~~~~~~~~~~~~~~~~**

    **~~~~~~~~~~~~~~~~~~~~~~a~~~~~** **~~~~~~~~~~~~~~~~~~~~~~~~~~~~** **~~~~~~~~~~~~~~~~~~~~~++++~~~** **~~~~~~~~~~~~~~~~~~~++++~~~** ***~~~~~~~~~~~~~~~++++~~~*** ****~~~~~~~~~~++++~~**** *****~~~~~~~~~***** ************* _ __ ___ ___ ___ \ \ / / /\ | |_) / / \ | |_) \_\/ /_/--\ |_| \_\_/ |_| \ a web framework for Swift Project "app-store-reviews" has been created. Type `cd app-store-reviews` to enter the project directory. Use `vapor cloud deploy` to host your project for free! Enjoy!
  29. @pdamonkey // swift-tools-version:4.0 import PackageDescription let package = Package( name:

    "app-store-reviews", dependencies: [ // " A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), // # A 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"]) ] )
  30. @pdamonkey Reviews Text Output (2) - Confusing!!! > Why I

    am asked to add apps that use up my storage & contribute with very little help. Constantly told “something not working or check out settings, privacy & security”. This is alarming, many options very confusing. Not helpful. - (1) - Crap > In the section “ get more help” you cannot submit your issues. WTF is the point of this app?????