Search code examples
iosswiftpromisealamofirepromisekit

Returning a Promise from PromiseKit/Alamofire


I'm trying to do this seemingly trivial thing:

static func list() -> Promise<[Activity]> {
    let endpoint = "\(self.baseUrl)/v1/activities"

    return Promise { fulfill, reject in
        self.fetchHeaders { (headers) in
            return Alamofire.request(
                endpoint,
                method: .get,
                parameters: nil,
                encoding: JSONEncoding.default,
                headers: headers
            ).validate().responseJSON().then() { response in
                guard let json = response as? JSON else {
                    reject(ActivityError.parse("Malformed JSON"))
                }

                guard let jsonActivities = json["activities"] as? [JSON] else {
                    reject(ActivityError.parse("Missing field"))
                }

                var activities: [Activity] = []

                for jsonActivity in jsonActivities {
                    guard let activity = Activity(json: jsonActivity) else {
                        reject(ActivityError.parse("Unable to parse an Activity object"))
                    }

                    activities.append(activity)
                }

                fulfill(activities)
            }.catch { error in
                reject(ActivityError.network("HTTP response failure"))
            }
        }
    }
}

However, the compiler (rightfully) complains that:

'guard' body may not fall through, consider using 'return' or 'break' to exit the scope

I understand I need to return a Promise here. I just can't figure out what exactly to put below the reject() and fulfill() calls.


Solution

  • There's nothing wrong with reject or fulfill calls. The issue is that after you reject in your guard statement, you also have to return to exit the closure:

    guard let json = response as? JSON else {
        reject(ActivityError.parse("Malformed JSON"))
        return
    }
    
    guard let jsonActivities = json["activities"] as? [JSON] else {
        reject(ActivityError.parse("Missing field"))
        return
    }
    

    The key point is that you do not want to conflate the promise that is returned by this method (which is later satisfied by fulfill or reject), with the fact that within this closure, you have to immediately exit the closure with a return in the guard clause.


    I cannot reproduce this issue with your code (because you haven't provided a MCVE and there are references here that I cannot resolve). But here is a simplified rendition of your code illustrating the use of guard:

    So, if not using PromiseKit/Alamofire, you can do:

    func list() -> Promise<[String: Any]> {
        return Promise { fulfill, reject in
            Alamofire.request(url)
                .validate()
                .responseJSON { response in
                    switch response.result {
                    case .success(let json):
                        guard let dictionary = json as? [String: Any] else {
                            reject(ActivityError.malformed("not a dictionary"))
                            return
                        }
                        fulfill(dictionary)
                    case .failure(let error):
                        reject(error)
                    }
            }
        }
    }
    

    As you can see, you are returning a Promise, but inside the Alamofire closure, you simply are exiting your guard statement.


    If you're using PromiseKit/Alamofire and call then, you presumably want to create a promise that it can return, such as:

    func list() -> Promise<String> {
        return Alamofire.request(endPoint)
            .validate()
            .responseJSON()
            .then { value in
                return Promise { fulfill, reject in
                    guard let dictionary = value as? [String: Any], let name = dictionary["name"] as? String else {
                        reject(ActivityError.malformed("not dictionary"))
                        return
                    }
    
                    fulfill(name)
                }
        }
    }
    

    Or if that's too hairy, you can pull out that parsing of the value:

    func list() -> Promise<String> {
        return Alamofire.request(endPoint)
            .validate()
            .responseJSON()
            .then { value in
                self.parse(value)
        }
    }
    
    func parse(_ value: Any) -> Promise<String> {
        return Promise { fulfill, reject in
            guard let dictionary = value as? [String: Any], let name = dictionary["name"] as? String else {
                reject(ActivityError.malformed("not dictionary"))
                return
            }
    
            fulfill(name)
        }
    }
    

    But, either way, even when using PromiseKit/Alamofire, you still just return within the guard clause.