How to wait on a future that returns void? - swift

I am following the book "Server Side Swift Vapor Edition" and I am trying to work on the exercises at page 174.
I have a struct called Poll, defined this way:
struct Poll: Content, SQLiteUUIDModel, Migration {
var id: UUID?
var title: String
var option1: String
var option2: String
var votes1: Int
var votes2: Int
}
It is mapped into a SQLite database with Fluent, and I am trying to write a route that given a post request like this one:
localhost:8080/polls/delete/
Is able to find the poll object in the database, return an error if it doesn't exist, or delete if it exists. This is how I am solving the problem at the moment:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).map(to: Poll.self) { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
poll.delete(on: req)
return poll
}
}
Let's break it down:
I read for the UUID passed in the post request (e.g. http://localhost:8080/polls/delete/FBF7FDC2-0ECB-4C1F-AD8F-A62DE68E531B)
I try to find the poll
If the poll object is nil (which means that it was not found), I throw a 404 not found error
If I find it, I delete the poll and I return it
This works. I am able to delete polls by using this route.
But I have some questions in my mind:
Can it be that the delete method still fails? (e.g. because of an internal SQLite error)
If yes, can I wait until the poll is actually deleted before returning it?
The problem is that the delete method returns an object of type EventLoopFuture. If it was an object of tupe EventLoopFuture, I would be able to easily map it to a poll. But being that the template argument is Void, if I modify the code this way:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).flatMap(to: Poll.self) { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
return poll.delete(on: req).flatMap(to: Poll.self) { poll -> EventLoopFuture<Poll> in
return poll
}
}
}
I get a syntax error: "Cannot convert value of type 'Void' to closure result type 'EventLoopFuture'". It looks like I am not able to map an EventLoopFuture object to EventLoopFuture. The problem is just that I want to wait for the delete operation to complete before returning. Any solution?

The delete function returns Void so your closure shouldn't have a parameter, which is one reason you are getting the syntax error. Try this:
router.post("polls", "delete", UUID.parameter) { req -> Future<Poll> in
let id = try req.parameters.next(UUID.self)
return Poll.find(id, on: req).flatMap { poll in
guard let poll = poll else {
throw Abort(.notFound)
}
return poll.delete(on: req).flatMap{
return request.future(poll)
}
}
}
Taking OP's comments to #Rob Napier's answer on-board, this should convert your poll back to a future but only after the delete has completed.

Related

Code improvement - how to serve use pictures in Vapor?

I have a working directory that contains every user's picture and I am trying to implement a call that returns data containing the user's picture, defined in this structure:
struct ImageData: Content {
var picture: Data // UIImage data
}
I tried to implement a solution also partially using what I found in the book 'Server Side Swift with Vapor' (version 3) in chapter 26 but that's different for me because I am not using Leaf and I need to return the data directly.
I came up with this function to return the user picture, which does its job but I am trying to improve it.
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<ImageData> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// To do: throw error (flatMapThrowing?)
let filename = user.profilePicture!
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
// Improvement: Do I need this?
var data = Data()
return req.fileio.readFile(at: path) { buffer -> EventLoopFuture<Void> in
let additionalData = Data(buffer: buffer)
data.append(contentsOf: additionalData)
return req.eventLoop.makeSucceededVoidFuture()
}.map {
return ImageData(picture: data)
}
}
}
First:
How to implement this using flatMapThrowing? If I replace flatMap with flatMapThrowing I get this error: "Cannot convert return expression of type 'EventLoopFuture' to return type 'ImageData'". Which doesn't make sense to me considering that flatMap allows returning a future and not a value.
I didn't find any solution other than using a Data variable and appending chunks of data as more data is read. I am not sure that this is thread-safe, FIFO and I don't consider it an elegant solution. Does anybody know any better way of doing it?
The short answer is that as soon as you have the file path, Vapor can handle it all for you:
func getProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<Response> {
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.tryflatMap { user in
// To do: throw error (flatMapThrowing?)
guard let filename = user.profilePicture else {
throw Abort(.notFound)
}
let path = req.application.directory.workingDirectory
+ imageFolder
+ filename
return req.fileio.streamFile(at: path)
}
}
You can use tryFlatMap to have a flatMap that can throw and you want to return a Response. Manually messing around with Data is not usually a good idea.
However, the better answers are use async/await and the FileMiddleware as two tools to clean up your code and remove the handler altogether

Ambiguous reference to member 'save(_:completionHandler:)' with CloudKit save attempt

I'm trying to save back to CloudKit after updating a reference list and getting the error on the first line of this code block.
Error: Ambiguous reference to member 'save(_:completionHandler:)'
CKContainer.default().publicCloudDatabase.save(establishment) { [unowned self] record, error in
DispatchQueue.main.async {
if let error = error {
print("error handling to come")
} else {
print("success")
}
}
}
This sits within a function where the user going to follow a given location (Establishment). We're taking the existing establishment, and its record of followers, checking to see if the selected user is in it, and appending them to the list if not (or creating it if the list of followers is null).
Edit, in case helpful
//Both of these are passed in from the prior view controller
var establishment: Establishment?
var loggedInUserID: String?
#objc func addTapped() {
// in here, we want to take the logged in user's ID and append it to the list of people that want to follow this establishment
// which is a CK Record Reference
let userID = CKRecord.ID(recordName: loggedInUserID!)
var establishmentTemp: Establishment? = establishment
var followers: [CKRecord.Reference]? = establishmentTemp?.followers
let reference = CKRecord.Reference(recordID: userID, action: CKRecord_Reference_Action.none)
if followers != nil {
if !followers!.contains(reference) {
establishmentTemp?.followers?.append(reference)
}
} else {
followers = [reference]
establishmentTemp?.followers = followers
establishment = establishmentTemp
}
[this is where the CKContainer.default.....save block pasted at the top of the question comes in]
I've looked through the various posts on 'ambiguous reference' but haven't been able to figure out the source of my issue. tried to explicitly set the types for establisthmentTemp and followers in case that was the issue (based on the solutions to other related posts) but no luck.
Afraid I'm out of ideas as a relatively inexperienced newbie!
Help appreciated.
Documenting the solution that I figured out:
Combination of two issues:
I was trying to save an updated version of a CK Record instead of updating
I was not passing a CK Record to the save() call - but a custom object
(I believe point two was the cause of the 'ambiguous reference to member'
error)
I solved it by replacing the save attempt (first block of code in the question) with:
//first get the record ID for the current establishment that is to be updated
let establishmentRecordID = establishment?.id
//then fetch the item from CK
CKContainer.default().publicCloudDatabase.fetch(withRecordID: establishmentRecordID!) { updatedRecord, error in
if let error = error {
print("error handling to come")
} else {
//then update the 'people' array with the revised one
updatedRecord!.setObject(followers as __CKRecordObjCValue?, forKey: "people")
//then save it
CKContainer.default().publicCloudDatabase.save(updatedRecord!) { savedRecord, error in
}
}
}

Vapor 3 - Returning a different Future when a search fails?

I'm using Vapor 3 and linking to a FoundationDB database, so Im not using Fluent. I have a method that searches for a record, but it will obviously crash if it does not return a record (because I force unwrap the value).
I want to guard the reading from the database and return a response if no record is found. This will however not be the record as was expected in the Future. I was thinking that I should return a different response, but am unsure how to change the result that is expected.
//creates a specific country
func getCountry( req: Request) throws -> Future<Country> {
// get Country name from get parameter string
let countryString = try req.parameters.next(String.self)
// get record from Database. This could fail and so needs to be guarded. What response should be returned as the Future requires a Country datatype?
let record = FDBConnector().getRecord(path: Subspace("iVendor").subspace(["Countries", countryString]))
let newCountry = try JSONDecoder().decode(Country.self, from: record!)
// return Country Struct
return Future.map(on: req) {return newCountry }
}
There are a couple of options here.
First, if you throw an error from the method:
guard let record = FDBConnector().getRecord(path: Subspace("iVendor").subspace(["Countries", countryString])) else {
throw Abort(.notFound, reason: "No country found with name \(countryString)")
}
The error will get converted to a 404 (Not Found) response with "No country found with name \(countryString)" as the error message.
If you want more control over the resulting response, you can change the route hander's return type to Future<Response>. You can then encode the Country object to the response or create a custom error response. This method does take some extra work though.
let response = Response(using: req)
guard let record = FDBConnector().getRecord(path: Subspace("iVendor").subspace(["Countries", countryString])) else {
try response.content.encode(["message": "Country not found"])
response.http.status = .notFound
return response
}
try response.content.encode(record)
return response
Note that you will have to conform Country to Content if you want that snippet to work.

Recursive/looping NSURLSession async completion handlers

The API I use requires multiple requests to get search results. It's designed this way because searches can take a long time (> 5min). The initial response comes back immediately with metadata about the search, and that metadata is used in follow up requests until the search is complete. I do not control the API.
1st request is a POST to https://api.com/sessions/search/
The response to this request contains a cookie and metadata about the search. The important fields in this response are the search_cookie (a String) and search_completed_pct (an Int)
2nd request is a POST to https://api.com/sessions/results/ with the search_cookie appended to the URL. eg https://api.com/sessions/results/c601eeb7872b7+0
The response to the 2nd request will contain either:
The search results if the query has completed (aka search_completed_pct == 100)
Metadata about the progress of search, search_completed_pct is the progress of the search and will be between 0 and 100.
If the search is not complete, I want to make a request every 5 seconds until it's complete (aka search_completed_pct == 100)
I've found numerous posts here that are similar, many use Dispatch Groups and for loops, but that approach did not work for me. I've tried a while loop and had issues with variable scoping. Dispatch groups also didn't work for me. This smelled like the wrong way to go, but I'm not sure.
I'm looking for the proper design to make these recursive calls. Should I use delegates or are closures + loop the way to go? I've hit a wall and need some help.
The code below is the general idea of what I've tried (edited for clarity. No dispatch_groups(), error handling, json parsing, etc.)
Viewcontroller.swift
apiObj.sessionSearch(domain) { result in
Log.info!.message("result: \(result)")
})
ApiObj.swift
func sessionSearch(domain: String, sessionCompletion: (result: SearchResult) -> ()) {
// Make request to /search/ url
let task = session.dataTaskWithRequest(request) { data, response, error in
let searchCookie = parseCookieFromResponse(data!)
********* pseudo code **************
var progress: Int = 0
var results = SearchResults()
while (progress != 100) {
// Make requests to /results/ until search is complete
self.getResults(searchCookie) { searchResults in
progress = searchResults.search_pct_complete
if (searchResults == 100) {
completion(searchResults)
} else {
sleep(5 seconds)
} //if
} //self.getResults()
} //while
********* pseudo code ************
} //session.dataTaskWithRequest(
task.resume()
}
func getResults(cookie: String, completion: (searchResults: NSDictionary) -> ())
let request = buildRequest((domain), url: NSURL(string: ResultsUrl)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
let theResults = getJSONFromData(data!)
completion(theResults)
}
task.resume()
}
Well first off, it seems weird that there is no API with a GET request which simply returns the result - even if this may take minutes. But, as you mentioned, you cannot change the API.
So, according to your description, we need to issue a request which effectively "polls" the server. We do this until we retrieved a Search object which is completed.
So, a viable approach would purposely define the following functions and classes:
A protocol for the "Search" object returned from the server:
public protocol SearchType {
var searchID: String { get }
var isCompleted: Bool { get }
var progress: Double { get }
var result: AnyObject? { get }
}
A concrete struct or class is used on the client side.
An asynchronous function which issues a request to the server in order to create the search object (your #1 POST request):
func createSearch(completion: (SearchType?, ErrorType?) -> () )
Then another asynchronous function which fetches a "Search" object and potentially the result if it is complete:
func fetchSearch(searchID: String, completion: (SearchType?, ErrorType?) -> () )
Now, an asynchronous function which fetches the result for a certain "searchID" (your "search_cookie") - and internally implements the polling:
func fetchResult(searchID: String, completion: (AnyObject?, ErrorType?) -> () )
The implementation of fetchResult may now look as follows:
func fetchResult(searchID: String,
completion: (AnyObject?, ErrorType?) -> () ) {
func poll() {
fetchSearch(searchID) { (search, error) in
if let search = search {
if search.isCompleted {
completion(search.result!, nil)
} else {
delay(1.0, f: poll)
}
} else {
completion(nil, error)
}
}
}
poll()
}
This approach uses a local function poll for implementing the polling feature. poll calls fetchSearch and when it finishes it checks whether the search is complete. If not it delays for certain amount of duration and then calls poll again. This looks like a recursive call, but actually it isn't since poll already finished when it is called again. A local function seems appropriate for this kind of approach.
The function delay simply waits for the specified amount of seconds and then calls the provided closure. delay can be easily implemented in terms of dispatch_after or a with a cancelable dispatch timer (we need later implement cancellation).
I'm not showing how to implement createSearch and fetchSearch. These may be easily implemented using a third party network library or can be easily implemented based on NSURLSession.
Conclusion:
What might become a bit cumbersome, is to implement error handling and cancellation, and also dealing with all the completion handlers. In order to solve this problem in a concise and elegant manner I would suggest to utilise a helper library which implements "Promises" or "Futures" - or try to solve it with Rx.
For example a viable implementation utilising "Scala-like" futures:
func fetchResult(searchID: String) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, f: poll)
}
}
}
poll()
return promise.future!
}
You would start to obtain a result as shown below:
createSearch().flatMap { search in
fetchResult(search.searchID).map { result in
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
This above contains complete error handling. It does not yet contain cancellation. Your really need to implement a way to cancel the request, otherwise the polling may not be stopped.
A solution implementing cancellation utilising a "CancellationToken" may look as follows:
func fetchResult(searchID: String,
cancellationToken ct: CancellationToken) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID, cancellationToken: ct).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, cancellationToken: ct) { ct in
if ct.isCancelled {
promise.reject(CancellationError.Cancelled)
} else {
poll()
}
}
}
}
}
poll()
return promise.future!
}
And it may be called:
let cr = CancellationRequest()
let ct = cr.token
createSearch(cancellationToken: ct).flatMap { search in
fetchResult(search.searchID, cancellationToken: ct).map { result in
// if we reach here, we got a result
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
Later you can cancel the request as shown below:
cr.cancel()

Downloaded Data does not print in order using GCDs in Swift

I am trying to have the downloaded return message print before the message "second". Basically, once the message has been downloaded it should print and then the "second" message. Everytime the code runs, the second message prints and then the returnMessage because the return message takes a bit to download. Is it possible to allow the return message to fire after it completes and then the second message everytime the code is run?
var returnMessage: String? = ""
var downloadGroup = dispatch_group_create()
dispatch_async(utility.GlobalUtilityQueue){
dispatch_group_enter(downloadGroup)
service.executeQuery(query, completionHandler: { (ticket: GTLServiceTicket!, object: AnyObject!, error: NSError!) -> Void in
// Process the response
let json = JSON(object.JSON)
returnMessage = json["message"].string
println("\(returnMessage)") // print first
})
dispatch_group_leave(downloadGroup)
dispatch_group_notify(downloadGroup, self.utility.GlobalMainQueue) {
println("second")//should print second
}
}
The problem is that the dispatch_group_leave should be inside the completionHandler of executeQuery.
var returnMessage: String? = ""
let downloadGroup = dispatch_group_create()
dispatch_async(utility.GlobalUtilityQueue){
dispatch_group_enter(downloadGroup)
service.executeQuery(query) { ticket, object, error in
// Process the response
let json = JSON(object.JSON)
returnMessage = json["message"].string
println("\(returnMessage)") // will print first
dispatch_group_leave(downloadGroup)
}
dispatch_group_notify(downloadGroup, self.utility.GlobalMainQueue) {
println("second")//will print second
}
}
Obviously, this is not a situation where you would use dispatch group (you'd generally only do it if you were entering and leaving multiple times). Also, the outer dispatch_async is probably unnecessary (you're calling an asynchronous method, so there's no need to dispatch that to some background queue). But I assume this was more of an academic question, so hopefully this helps.