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
- Download the example project here.↩
- If you do not have Vapor installed already, you can follow this tutorial.↩
- The list of female laureates are extracted from here.↩
- 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]↩
Author
Heidi Puk Hermann
Vapor Developer