Problem identifying correct closure

I am trying to migrate a Vapor 2 application to Vapor 3 and am running into a seemingly intractable problem. I need to include a field from a parent relation in a Leaf view of a record set. I can do it if I use a join and merge the tuple manually, but this seems like a lot of work compared to the ease of doing it in Vapor 2. I have been wrestling with the compiler of the best part of 24 hours trying to get the following to the point where it will let me debug it.

The error:

Cannot convert value of type '[EventContext]' to closure result type 'EventLoopFuture<[EventContext]>'

arises in the code as follows, I have commented the specific place/line:

events.get("oops")
{
    request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    return query.all().flatMap
    {
        events -> Future<[EventContext]> in
        return events.map // error points to map on this line
        {
            event -> EventContext in
            let loc = event.location.query(on:request).first().map { return $0!.name }.unwrap(or:Abort(HTTPResponseStatus.notFound))
            return EventContext(date:event.date, name:event.name, location:loc)
        }
    }
}

struct EventContext: Encodable, Content {
    let date: Date
    let name: String
    let location: String
}

I have tried every possible permutation of map vs. flatMap, Future and Future<[EventContext]> I can think of - and probably most of them several times. I have even managed to get the compiler to segfault with some of them. Can anyone see what the problem is, please?

Your primary issue is the returned type of events.map.

Consider the closure passed to query.all().flatMap. You've declared a type signature that claims that this closure will return a value of type Future<[EventContext]>. However, that cannot be return value here. This is because the type of the value of the argument events is [RangeEvent], which means that events.map must return an array, not a Future of an array.

You have a few other issues here though, most notably your construction of EventContext. The particular risk is that loc is not of type String, it's of type Future<String>. You need to await that result first.

A better construction might be this:

events.get("oops") { request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    
    return query.all().flatMap { events -> Future<[EventContext]> in
        let futureContexts = events.map { event -> Future<EventContext> in
            return event.location.query(on:request).first().unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMap { location in
                return EventContext(date: event.date, name: event.name, location: location.name }
            }
        }
        return futureContexts.flatten()
    }
}

This is still a bit more complex than my ideal formulation, but it should compile.

Hi. Thank you for the response. I had tried that closure type already, it gives the error:

Cannot convert value of type '(RangeLocation) -> EventContext' to expected argument type '(RangeLocation) -> EventLoopFuture<_>'

In the line marked in the code below:

events.get("oops")
{
    request -> Future<[EventContext]> in
    var query = RangeEvent.query(on:request)
    
    return query.all().flatMap
    {
        events -> Future<[EventContext]> in
        let futureContexts = events.map
        {
            event -> Future<EventContext> in
            return event.location.query(on:request).first().unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMap
            { // error marked on opening brace
                location in // MARK
                return EventContext(date: event.date, name: event.name, location: location.name) // NEW
            }
        }
        return futureContexts.flatten()
    }
}

If I try putting -> Future<EventContext> into the line MARK then it moves the error below to line NEW:

Cannot convert value of type 'EventContext' to closure result type 'EventLoopFuture<EventContext>'

If I remove Future from the inner two, the error becomes:

Cannot convert value of type '(RangeLocation) -> EventContext' to expected argument type '(RangeLocation) -> EventLoopFuture<_>'

I take your point about location ideally being a Future but if you put that into the struct EventContext it won't compile because Future is not Decodable.

I have managed to fix it! I would be interested in feedback as to whether or not it is genuinely async, but it feels right.

events.get("oops")
{
    request -> Future<View> in
    let query = RangeEvent.query(on:request)
    return query.all().flatMap
    {
        events in
        let futureContexts = events.map
        {
            event -> EventContext in
            let locationName = event.location.query(on:request).first().map
            {
                location in
                return location.flatMap { return $0.name }!
            }
            return EventContext(date: event.date, name: event.name, location: locationName )
        }
        let context = AllEventsContext(title: "Demo", hidePastEvents: hidePastEvents, events:futureContexts)
        return try request.view().render("events",context)
    }
}

If it works, you're probably good! Until Swift gets a concurrency model and async/await it's going to be like this unfortunately.

However, a few things to note - if you're passing contexts to Leaf, they can be Encodable so you can pass Leaf futures. The best way to remember whether to use flatMap or map is if the closure returns a Future use flatMap, otherwise use map. If you're doing any mapping or flatMapping inside the closure, since that returns a Future, the method you pass the closure to will be flatMap - hope that helps!