Using Vapor to iteratively call a method the returns a Future

Viewed 260

I am using Swift 5 and Vapor 3. I have written a client to call Twitter to get followers of a user. It looks like this

func followersOf(_ screenName : String, nextCursor : Int64 = -1) throws -> Future<UserCursor> {
    logger.debug("Fetching followers of \(screenName)")
    let res = httpClient.get("https://api.twitter.com/1.1/followers/list.json?screen_name=\(screenName)&nextCursor=\(nextCursor)", headers: ["authorization": authToken])
    return res.flatMap { res in
        return try res.content.decode(UserCursor.self, using: self.jsonDecoder)
    }
}

UserCursor returns a value for the nextCursor and a list of Users for for the page that was fetched. I need to keep calling this method with the value of nextCursor and accumulating the Users for each page until nextCursor returns -1. How would I use the Future returned from this method to iteratively call it until I have visited all the pages of the cursor while accumulating the Users returned from each call?

This is what I have so far but I'm at a loss. I feel I'm way off the mark.

func followersOf(_ req : Request) throws -> Future<FollowersView> {
    let logger = try req.make(Logger.self)
    let screenName = try req.parameters.next(String.self)

    logger.debug("Request for followers of \(screenName)")

    let twitter = try req.make(TwitterClient.self)
    return try twitter.followersOf(screenName).flatMap { userCursor in
        var uc = userCursor
        var users : Set<User> = []
        users = users.union(userCursor.users)
        while (uc.nextCursor != -1) {
            try twitter.followersOf(screenName, nextCursor: userCursor.nextCursor).map { uc in uc}
        }
        return FollowersView(screenName, users)
    }
}
1 Answers

I think inside twitter you could make a private _followersFetcher method which will call _followers until it get -1 cursor, and public fetchFollowers method which will have a deal with fetcher, something like this:

import Vapor

class TwitterClient : Service {
    private let authToken : String
    var httpClient : Client

    let jsonDecoder : JSONDecoder
    let logger : Logger
    let eventLoop : EventLoop

    init(_ client : Client, _ logger : Logger) throws {
        jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase

        guard let apiToken = Environment.get("TWITTER_TOKEN") else {
            throw Abort(.internalServerError)
        }

        authToken =  "Bearer " + apiToken

        self.logger = logger

        self.httpClient = client

        self.eventLoop = httpClient.container.eventLoop
    }

    private func _followers(of screenName : String, nextCursor : Int64 = -1) throws -> Future<UserCursor>{
        logger.debug("Fetching followers of \(screenName) cursor \(nextCursor)")
        let res = httpClient.get("https://api.twitter.com/1.1/followers/list.json?screen_name=\(screenName)&cursor=\(nextCursor)", headers: ["authorization": authToken])
        return res.flatMap { res in
            return try res.content.decode(UserCursor.self, using: self.jsonDecoder)
        }
    }

    private func _followersFetcher(of screenName : String, nextCursor : Int64 = -1, users: Set<User> = []) throws -> Future<UserCursor> {
        return try _followers(of: screenName, nextCursor: nextCursor).flatMap {
            let newUsers = users.union($0.users)
            if $0.nextCursor > 0 {
                return try self._followersFetcher(of: screenName, nextCursor: $0.nextCursor, users: newUsers).map {$0}
            }
            return self.eventLoop.future(UserCursor(users: newUsers.map{$0}))
        }
    }

    func fetchFollwers(of screenName : String) throws -> Future<[User]> {
        return try _followersFetcher(of: screenName).map{$0.users}
    }
}

With Vapor and NIO it is very important to stay on eventLoop all the time. In example above _followersFetcher calls itself as many times as needed to get all the users and only then returns the result.

You could rewrite code maybe to make it look more clean/elegant, but I think this is the only technique available in situation when you're getting next cursor only after query with previous.

In case if you'd have a list of cursors in advance you could simply use flatten

private func _followersFetcher(of screenName : String, cursors: [Int64]) throws -> Future<[User]> {
    var users: Set<User> = []
    return cursors.map {
        _followers(of: screenName, nextCursor: $0).map {
            users.union($0.users)
        }
    }.flatten(on: eventLoop).map { users.map { $0 } }
}
Related