Color Mode


    Language

Fetching files easily in Vapor when you are writing tests

August 15, 2019

Vapor has a nifty build-in feature to derive the working directory of a project. This makes it easy for you to fetch files from your project and serve their content; fx if you want to seed some data in your database, if you are building an initial mock api at the beginning of a project, if you want to ease the process of testing data parsing, or something fourth.
In this tutorial we'll use Vapor's build in function to derive the working directory and thereby easily fetch files from our project. We'll then decode the content of our file into a custom Swift object and test that it works as expected1.

Get started

First we'll create a new Vapor project in the terminal2

vapor new nobel-laureates-example

next we'll relocate into the project folder and generate our Xcode project.

cd nobel-laureates-example/
vapor xcode -y

You now have a fully functioning Vapor project; All you have to do is run it.

Where to store your files

Next step is to decide where to store our files. Our newly created Vapor project came with a number of predefined folders. The app itself goes into Sources, while all test should be in Tests, finally are the Public and Resources folders. (You may not have the Resources by default, but then you can just create it yourself).
The Public folder should only be used for files that you want anybody from outside of the app to get access to. Those could be your CSS and Javascript files for your frontend or a document anybody should be able to download.
Resources is for files that you do not want the outside to gain access to (but at the same time not super secret files that need encryption, password etc.).
We will be storing our files in Resources for this tutorial.

Fetching the file

Add a sub-folder called Utilities inside Sources. This is for any general methods we'll be needing during the tutorial. Inside Utilities we'll add a new Swift file called "Data+fromJSONFile.swift".

First we'll import Core so we can get easy access to the working directory. Core is a utility package from Vapor that contains tools for byte manipulation, Codable, OS APIs, and debugging. Then we'll extend Data with a method, which we'll call fromFile.

fromFile(_:,folder:) takes the file name and the relative location of the file as arguments. It then returns the content of the file as Data (Since we are only going to work with json files in this tutorial, we'll add the relative path to the json folder as a default value). If no file exists with the provided name and location the method will throw an error.

import Core

extension Data {
    static func fromFile(
        _ fileName: String,
        folder: String = "Resources/json"
    ) throws -> Data {
        let directory = DirectoryConfig.detect()
        let fileURL = URL(fileURLWithPath: directory.workDir)
            .appendingPathComponent(folder, isDirectory: true)
            .appendingPathComponent(fileName, isDirectory: false)

        return try Data(contentsOf: fileURL)
    }
}

We use Vapor's DirectoryConfig.detect() to get the location of the working directory of the project. From here we can use Swift's URL type initialized by providing the file URL, URL((fileURLWithPath: String). In the end, the URL will look like the following:

"/[working-directory]/[folder]/[fileName]"

Now that we have the full address of the file, we can fetch it and return it as Data.

So let's test what happens when we provide a correct and a wrong file name, respectively:

func testCorrectFileName() throws {
    let fileName = "femaleNobelLaureates.json"
    XCTAssertNotNil(try Data.fromFile(fileName)) // Success
}

func testWrongFileName() throws {
    let fileName = "femaleNobelLaureate.json"
    XCTAssertThrowsError(try Data.fromFile(fileName)) // Success
}

In the first test the correct file name is provided and the content of the file is encode into Data. In the second test there is a spelling error and so the method throws the following error, as expected: "The file “femaleNobelLaureate.json” couldn’t be opened because there is no such file.".

Decoding into a custom swift object

For the rest of the blog we'll be working with a list of all female Nobel Laureates in the categories Physics, Chemistry and Physiology or Medicine3. You can find the json file here.
Each scientific accomplishment is summed down to a json dictionary that looks like the following:

{
  "id": 19,
  "fullName": "Donna Strickland",
  "category": "physics",
  "year": 2018,
  "rationale": "for their method of generating high-intensity, ultra-short optical pulses",
  "isShared": true
}

We'll be using Swift's Codable protocol for easy decoding of the data into an array of our custom object. For that we'll set up a class called NobelLaureates that has the same six variables as in the json dictionary and add a memberwise initializer. And so, the model is ready.

final class NobelLaureates: Codable {
    var id: Int
    var fullName: String
    var category: String
    var year: Int
    var rationale: String
    var isShared: Bool

    ... // memberwise initializer has been left out here
}

Next we'll set up our custom decoder, which we need for decoding the data. We'll add it in an extension to NobelLaureates along with a private struct that functions as a container for the decoded data.

extension NobelLaureates {
    static func loadFromFile(
      _ fileName: String = "femaleNobelLaureates.json"
    ) throws -> [NobelLaureates] {
        let decoder = JSONDecoder()
        let laureatesData = try Data.fromFile(fileName)
        let decodedNobelLaureates = try decoder.decode(
            LaureatesDecoderObject.self,
            from: laureatesData
        )
        return decodedNobelLaureates.data
    }

    private struct LaureatesDecoderObject: Content {
        var data: [NobelLaureates]
    }
}

Okay, we are now ready to test if the decoder is working. First we'll test that we have decoded the correct number of recipients of the Nobel price in Physics, Chemistry and Medicine or Physiology, which is 20.

func testDecodeLaureatesCount() throws {
    let testData = try NobelLaureates.decodeFromData()
    XCTAssertEqual(testData.data.count, 20) // Success
}

Next we'll test who was the seventh woman to receive a Nobel in the natural sciences4. Her name was Rosalyn Sussman Yalow. She received the price in 1977 for her work in developing radioimmunoassays of peptide hormones.

func testDecodeLaureatesAtIndex() throws {
    let testData = try NobelLaureates.decodeFromData()
    XCTAssertEqual(testData.data[6].fullName, "Rosalyn Sussman Yalow") // Success
    XCTAssertEqual(testData.data[6].year, 1977) // Success
}

Success, our decoder works as expected!

Final notes

In this tutorial we have created a convenient method for fetching files in our Vapor project. Next we created a custom decoder to convert the data in our json file to Swift objects. Finally we covered each step with tests to verify that they worked.
And a very final note; We used a json file in the tutorial, but the methods presented here can just as well be used to fetch and decode the content of a csv/xlsx/whatever file.

Article photo by Fabian Grohs


  1. Download the example project here.↩
  2. If you do not have Vapor installed already, you can follow this tutorial.↩
  3. The list of female laureates are extracted from here.↩
  4. Actually, Rosalyn Sussman Yalow was the sixth woman to receive a Nobel in the natural science topics, as Marie Skłodowska Curie receive the price twice. [Physics in 1903 and Chemistry in 1911]↩
vaporvapor 3swiftserver side swifttesting

Author

Heidi Puk Hermann

Heidi Puk Hermann

Vapor Developer

👩🏼‍💻 Physicist turned teacher turned developer... Passionate about learning and teaching

You may also like

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

May 27, 2024

Introducing UCL Max AltPlay, a turn-by-turn real-time Football simulation

At this year's MonstarHacks, our goal was to elevate the sports experience to the next level with cutting-edge AI and machine learning technologies. With that in mind, we designed a unique solution for football fans that will open up new dimensions for wa...

Rayhan NabiRokon UddinArman Morshed

Rayhan Nabi, Rokon Uddin, Arman Morshed

MonstarHacks

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service