Try Vapor

«  Swift String Literals
Protocol-Oriented Programming in Swift  »

I’ve heard Swift on Server for a while, but never try it before. So I’d like to try to use it and memo some in this blog. Here is some benefits they wrote in the doc:

  • Cloud services built with Swift have a small memory footprint (measured in MB)–especially when compared to other popular server languages with automatic memory management.
  • Services built with Swift are also CPU-efficient, given the language’s focus on performance.
  • Using Swift also helps streamline continuous delivery pipelines, since you incur less wait time for new versions of the service fleet to come online.
  • quick boot times make Swift a perfect fit for serveless applications such as Cloud Functions or Lambda with negligible cold start times.

After doing some research, I notice the major Swift framework is Vapor. That’s I’d like to try it at first. Basically I will follow with the guide, and will memo some if I met any error.

Hello World Vapor

# install
$ brew install vapor

# new project
$ vapor new Try-Vapor-Hello-World -n
$ cd Try-Vapor-Hello-World

# open in Xcode
$ open Package.swift

Run with My Mac target, check console:

[ NOTICE ] Server starting on http://127.0.0.1:8080

with ⬇️ routes settings

open http://127.0.0.1:8080/ ⬇️

open http://localhost:8080/hello ⬇️

It will also show request-id info in console as bellow ⬇️

Terminal

Swift Package can also run in terminal by using:

$ swift run

It will also build and run project. It also show dependency fetching info and at the end, show connection info like ⬇️

[1511/1511] Build complete!
[ NOTICE ] Server starting on http://127.0.0.1:8080

Here is my test project: https://github.com/HevaWu/Try-Vapor-Hello-World.

Shiba Random Vapor

Next step, I’d like to try to use Leaf in Vapor. Here is my test project: https://github.com/HevaWu/Vapor-Shiba-Random

It will run like:

Same as above, create one new project by:

$ vapor new Vapor-Shiba-Random
# select `y` at ``Would you like to use Leaf?``

The generated template have bellow problem for now:

  • routes.swift has a redundant route, we need to remove it to use
  • Package.swift target dependency missing Leaf module, we need to manually add it
  • manually add app.views.use(.leaf) in configure.swift to tell system we are using leaf

Next step, we want to build it, after running, we notice the http://127.0.0.1:8080/index show error:

open(file:oFlag:mode:): No such file or directory (errno: 2)

It means it cannot find proper leaf template place. For solving it, we need to set custom working directory to let vapor find it.

For now, the template paths should show well. We can try to add more template and render it now!

Add Welcome Page

First, for testing adding images and embedded child leaf, I add one /welcome for try.

I set my base.leaf and welcome.leaf as bellow:

<!-- base.leaf -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>#import("title")</title>
    <link rel="stylesheet" href="/styles/syntax.css">
</head>

<body>
    <h1>Hello! #import("title")</h1>
    #import("body")
</body>

</html>

<!-- welcome.leaf -->
#extend("base"):
    #export("title"):
    It works!
    #endexport

    #export("body"):
        <div class="welcome">
            <img src="/images/hello.png">
        </div>
    #endexport
#endextend

Here are several notes:

  • manually create /Public folder first, and put required server sharing files into it
  • by using /styles and /images, we need to enable /Public FileMiddleware first.
  • use extend, export, import for sharing data between parent and child pages. (if we directly search online, there are some site is using set get which vapor is not support them anymore)

Shiba List Page

Next Step, I’d like to fetch get request from the http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true.

NOTE: Vapor API changed a lot for each major version. For now, the Data(body.readableBytesView) works at here. But, this behavior might be changed in later version.

let future = req.client
    .get("http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true")
    .flatMapThrowing { res -> ShibaImageURLList? in
        guard let body = res.body else {
            return nil
        }
        let data = Data(body.readableBytesView)
        guard let jsonAsArr = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] else {
            return nil
        }

        let list = jsonAsArr.compactMap { object -> ShibaImageURL? in
            if let str = object as? String {
                return ShibaImageURL(urlString: str)
            } else {
                return nil
            }
        }
        return ShibaImageURLList(list: list)
    }

This code will return a EventLoopFuture<ShibaImageURLList?> object now. And next step will be show list on our website.

.flatMap { list in
    req.view.render("shibalist", list)
}

NOTE: NOT using req.view.render("shibalist", future). It will throw error about EventLoopFuture<Type> not from Encodable. This actually means we should not directly use future object, instead, use flatMap to filter and render object properly.

NOTE: We need to use a ShibaImageURLList to wrap final response at here. If we directly use [ShibaImageURL], system will throw error at the render page LeafKit.LeafError.Reason.unsupportedFeature(\"You must use a top level dictionary or type for the context. Arrays are not allowed.\") to inform we have to wrap it to a final response object.

For now, the list can show well now. But I’d like to try if we can clean some code by using Controller. Vapor’s Folder Structure doc mentioned:

Controllers are great way of grouping together application logic. Most controllers have many functions that accept a request and return some sort of response.

For this request, I tried to make a Controller like:

final class ShibaController {
    func index(req: Request) -> EventLoopFuture<View> {
        let uri = URI(string: "http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true")
        return req.client
            .get(uri)
            .flatMapThrowing { res -> ShibaImageURLList in
                guard let body = res.body else {
                    return ShibaImageURLList.empty
                }
                let data = Data(body.readableBytesView)
                guard let jsonAsArr = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] else {
                    return ShibaImageURLList.empty
                }

                let list = jsonAsArr
                    .compactMap { object -> ShibaImageURL? in
                        if let str = object as? String {
                            return ShibaImageURL(urlString: str)
                        } else {
                            return nil
                        }
                    }
                return ShibaImageURLList(list: list)
            }
            .flatMap { list in
                req.view.render("shibalist", list)
            }
    }
}

And call it in routes like:

let shibaController = ShibaController()
app.get("shibalist", use: shibaController.index(req:))

The rendered page is working same as previous code. Now, the page is good. But, we find we can customize the parameter in this GET request, ex: count, urls, etc. Maybe we can try to make a user input form, and parse parameter to this request to get list.

// define at Controller
func index(req: Request) -> EventLoopFuture<View> {
    return req.view.render("shibalistGetParam")
}

func getList(req: Request, uri: URI) -> EventLoopFuture<View> {
    return req.client
        .get(uri)
        ...
}

func postParams(req: Request) throws -> EventLoopFuture<View> {
    let content = try req.content.decode(ShibaListGetRequestContent.self)
    if var components = URLComponents(string: Self.baseURLString) {
        components.queryItems = [
            URLQueryItem(name: "count", value: content.count),
            URLQueryItem(name: "urls", value: content.urls.description),
            URLQueryItem(name: "httpUrls", value: content.httpsUrls.description),
        ]
        if let uriStr = components.string {
            let uri = URI(string: uriStr)
            return getList(req: req, uri: uri)
        }
    }

    // parse URI failed, back to welcome page
    return req.view.render("welcome")
}

// call in routes
app.post("shibalist-submit", use: shibaController.postParams(req:))

DONE!

The End

It’s very easy to use Vapor for generating some website, its logic is clean. But I’m thinking it might still under improvement/design, so its configuration is keep changing. We’d better to read its latest document to do our design, rather than search some existed project. (That’s why bump major version changes 🤣)

Well, in general, as a Swift programmer, I’m very happy to see we can use Swift on Server development, and I’d like to keep watching its updates! 🤠

Reference

Published on 19 Jun 2021 Find me on Facebook, Twitter!

«  Swift String Literals
Protocol-Oriented Programming in Swift  »

Comments

    Join the discussion for this article at here . Our comments is using Github Issues. All of posted comments will display at this page instantly.