Search code examples
swiftvapor

How to transform this side-effect into an elegant chaining of Vapor operations?


I am starting using Vapor to run an API, and I couldn't figure out an elegant way of doing the following: (1) load a Fluent relation and (2) wait for the response of an HTTP callback before (3) responding to the original request.

Here is what I ended up coding, after I transformed my real-life entities into planets and stars to maintain the examples from Vapor's documentation. It works™️, but I couldn't achieve a nice chaining of operations ☹️.

    func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
        Planet(req.parameters.get("planetID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMap { planet in
                // I load the star because I need its ID for the HTTP callback
                _ = planet.$star.load(on: req.db).map {
                    let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
                    // HTTP Callback
                    req.client.post(uri) { req in
                        try req.content.encode(
                            WebhookDTO(
                                starId: planet.star.id!,
                                status: .humanFriendly,
                                planetId: planet.id!
                            ),
                            using: JSONEncoder()
                        )
                    }
                    .map { res in
                        debugPrint("\(res)")
                        return
                    }
                }

                // Couldn't find a way to wait for the response, so the HTTP callback is a side-effect
                // and its response is not used in the original HTTP response...
                // If the callback fails, my API can't report it. 
                planet.state = .humanFriendly
                return planet.save(on: req.db).map { planet }
            }
    }

Issue #1. Combining 2 EventLoopFuture

My first issue was that I couldn't find a way to load a parent relationship while keeping the entity in the scope.

Planet(req.parameters.get("planetID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { planet in // planet is in the scope 🎉
        return planet.$star.load(on: req.db) // returns a EventLoopFuture<Void>
    }
    .map {
      // I know that star has been loaded but I lost my `planet` reference ☹️
      ???
    }

I assume there is an operator that should be able to return a mix of the 2 EventLoopFuture instances but I couldn't figure it out.

Issue #2. Chaining EventLoopFuture with an auxiliary HTTP request's response

Here as well, I assume I miss an operator that allows me to wait for the request's response, while keeping a reference to the planet, before I respond to the original request.


Help would be welcome on how to achieve this in a nice chaining of operations — if it is possible, of course — and I will more than happy to update Vapor's documentation accordingly. 🙏


Solution

  • In short, if you need the result of one future inside the result of another future then you have to nest the callbacks - you can't use chaining. So for instance:

    Planet(req.parameters.get("planetID"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap { planet in // planet is in the scope 🎉
            return planet.$star.load(on: req.db).map {
                // Star is now eager loaded and you have access to planet
            }
        }
    

    If you have multiple futures that aren't reliant on each other but you need the results of both you can use and to chain the two together and wait for them. E.g.:

    func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
        Planet(req.parameters.get("planetID"), on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMap { planet in
                // I load the star because I need its ID for the HTTP callback
                planet.$star.load(on: req.db).map {
                    let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
                    // HTTP Callback
                    let postFuture = req.client.post(uri) { req in
                        try req.content.encode(
                            WebhookDTO(
                                starId: planet.star.id!,
                                status: .humanFriendly,
                                planetId: planet.id!
                            ),
                            using: JSONEncoder()
                        )
                    }.map { res in
                        debugPrint("\(res)")
                        return
                    }
    
                    planet.state = .humanFriendly
                    let planetUpdateFuture = planet.save(on: req.db)
    
                    return postFuture.and(planetUpdateFuture).flatMap { _, _ in
                        return planet
                    }
                }
            }
    }