Vapor 4: serving the contents of a JSON file as String

Hi there,

getting my feet wet with Vapor 4 I want to serve several JSON files as raw Data or String for that matter using Vapor 4.

I have a database which contains besides other stuff UUIDs which also act as filenames for several JSON files.

Currently my code looks like this:

routes.swift:

func routes(_ app: Application) throws {
    let geoJSONController = GeoJSONController()
    
    …

    app.get("MapEditorBackend", ":GeoJSONID", use: geoJSONController.get)
    
    …
}

GeoJSONController.swift:

struct GeoJSONController {
    …

    func get(req: Request) throws -> EventLoopFuture<String> {
        return GeoJSON.find(req.parameters.get("GeoJSONID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMap { String(decoding: Data.fromFile($0.id!.uuidString), as: UTF8.self) }
            .transform(to: .ok)
    }
    
    func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> {
        return GeoJSON.find(req.parameters.get("GeoJSONID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMap { $0.delete(on: req.db) }
            .transform(to: .ok)
    }
}

when compiling above code I get those Errors:

SLPN-NB-LSH:MapEditorBackend lars$ ~/toolbox-18.0.0-beta.27/vapor-beta run
/Users/lars/Documents/Projects/MapEditor/MapEditorBackend/MapEditorBackend/Sources/App/Controllers/GeoJSONController.swift:16:14: error: generic parameter 'NewValue' could not be inferred
            .unwrap(or: Abort(.notFound))
             ^
/Users/lars/Documents/Projects/MapEditor/MapEditorBackend/MapEditorBackend/.build/checkouts/swift-nio/Sources/NIO/EventLoopFuture.swift:465:17: note: in call to function 'flatMap(file:line:_:)'
    public func flatMap<NewValue>(file: StaticString = #file, line: UInt = #line, _ callback: @escaping (Value) -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
                ^
/Users/lars/Documents/Projects/MapEditor/MapEditorBackend/MapEditorBackend/Sources/App/Controllers/GeoJSONController.swift:17:24: error: cannot convert value of type 'String' to closure result type 'EventLoopFuture<NewValue>'
            .flatMap { String(decoding: Data.fromFile($0.id!.uuidString), as: UTF8.self) }
                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/lars/Documents/Projects/MapEditor/MapEditorBackend/MapEditorBackend/Sources/App/Controllers/GeoJSONController.swift:18:29: error: type 'EventLoopFuture<String>' has no member 'ok'
            .transform(to: .ok)
                           ~^~
[2/3] Compiling App routes.swift
Fatal error: result 1: file /Users/lars/toolbox-18.0.0-beta.27/Sources/VaporToolbox/exec.swift, line 50
Illegal instruction: 4

while the delete Function seems to work fine (I used it as a template for my get function) here EventLoopFuture<String> as a return type doesn't seem to work.

What I am doing wrong here? Do I have to wrap the String I create in the flatMap closure somehow?

Thanks in advance,

Lars

At a glance I would guess the problem is actually with your string construction. You may want String(data:encoding:) (which produces an optional you will need to unwrap) or you could go straight from the file to the string with a different constructor skipping data altogether. You also should be able to just pass the data to a Response constructor if you’d like to go that route, but you would need to change the signature of your get function in that case.

The String initializer is fine. Again just scanning from this forum thread, I think the problem is using flatMap there, flatMap expects you to return an EventLoopFuture but the String initializer just returns a string. Change flatMap to map in the get(req:) handler and that should clean up your build errors.

1 Like

Good catch. I change my vote to this.

Is that a valid use of .transform(to:)? You should get an HTTP 200 status just for successfully creating a String here so at best I think it is superfluous.

1 Like

Another good point! If you want to return the string, you need to remove the transform. The transform(to: .ok) drops the string completely and only returns the status code.

1 Like

Thanks to all for your quick help! Changing flatMap to just map and removing the transform almost fixed everything. Now my function looks like this:

    func get(req: Request) throws -> EventLoopFuture<String> {
        return GeoJSON.find(req.parameters.get("GeoJSONID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .map {
                if let data = try? Data.fromFile($0.id!.uuidString) {
                    return String(decoding: data, as: UTF8.self)
                } else {
                    return ""
                }
        }
    }

and works like a charm!

One more suggestion, you may want to be more explicit about the error case here, rather than simply swallowing with try?. You can do something like:

 func get(req: Request) throws -> EventLoopFuture<String> {
        return GeoJSON.find(req.parameters.get("GeoJSONID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMapThrowing { found in
                try Data.fromFile(found.id!.uuidString)
            }.map { data in 
                String(decoding: data, as: UTF8.self)
            }.flatMapErrorThrowing { error in 
                throw Abort(.internalServerError) // you could do anything in here, or omit `flatMapErrorThrowing ` and propagate the error from the `fromFile` call directly
            }
    }
1 Like