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
Related
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.
I need to make multiple calls.
1. Delete Document Upload
2. Image 1 & server returns URL
3. Upload Image 2 & server returns URL
4. Create Document API contains both URLs & extra
parameters.
The code which I tried to write is in RxSwift,& MVVM.
let resultOfDocumentUpdateWithDelete =
donepressed
.filter{ $0 }
.withLatestFrom(self.existingDocumentIDChangedProperty)
.flatMapLatest {id in
let deleted_document = apiClient.deleteDocument(id).asObservable().materialize()
let upload_frontImage = deleted_document
.withLatestFrom(self.frontImageNameChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_backImage = upload_frontImage
.withLatestFrom(self.backImageChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_document = upload_backImage
.withLatestFrom(self.parametersChangedProperty)
.flatMapLatest {parameters in
apiClient.uploadDocument(parameters: parameters)
}
return upload_document.materialize()
}
.share(replay: 1)
Make sure, two responses of server are input in last API, so all of these will be called in a sequence.
how to do in RxSwift.
This was an interesting one! The take-away here is that when you are in doubt, go ahead and make your own operator. If it turns out that you later figure out how to do the job using the built-in operators, then you can replace yours. The only thing with making your own is that they require a lot more testing.
Note, to use the below, you will have to combineLatest of your observables and then flatMap and pass their values into this function.
// all possible results from this job.
enum ProcessResult {
case success
case deleteFailure(Error)
case imageFailue(Error)
case backImageFailure(Error)
case documentFailure(Error)
}
func uploadContent(apiClient: APIClient, documentID: Int, frontImage: UIImage, backImage: UIImage, parameters: Parameters) -> Single<ProcessResult> {
// instead of trying to deal with all the materializes, I decided to turn it into a single process.
return Single.create { observer in
// each api call happens in turn. Note that there are no roll-back semantics included! You are dealing with a very poorly written server.
let deleted = apiClient.deleteDocument(id: documentID)
.asObservable()
.share()
let imagesUploaded = deleted
.flatMap { _ in Observable.zip(apiClient.uploadImage(image: frontImage).asObservable(), apiClient.uploadImage(image: backImage).asObservable()) }
.share()
let documentUploaded = imagesUploaded
.flatMap { arg -> Single<Void> in
let (frontURL, backURL) = arg
var updatedParams = parameters
// add frontURL and backURL to parameters
return apiClient.uploadDocument(parameters: updatedParams)
}
.share()
let disposable = deleted
.subscribe(onError: { observer(.success(ProcessResult.deleteFailure($0))) })
let disposable1 = imagesUploaded
.subscribe(onError: { observer(.success(ProcessResult.imageFailue($0))) })
let disposable2 = documentUploaded
.subscribe(
onNext: { observer(.success(ProcessResult.success)) },
onError: { observer(.success(ProcessResult.documentFailure($0))) }
)
return Disposables.create([disposable, disposable1, disposable2])
}
}
I have a recursive, async function that queries Google Drive for a file ID using the REST api and a completion handler:
func queryForFileId(query: GTLRDriveQuery_FilesList,
handler: #escaping FileIdCompletionHandler) {
service.executeQuery(query) { ticket, data, error in
if let error = error {
handler(nil, error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
self.queryForFileId(query: query, handler: handler)
} else if let id = list.files?.first?.identifier {
handler(id, nil)
} else {
handler(nil, nil) // no file found
}
}
}
}
Here, query is set up to return the nextPageToken and files(id) fields, service is an instance of GTLRDriveService, and FileIdCompletionHandler is just a typealias:
typealias FileIdCompletionHandler = (String?, Error?) -> Void
I've read how to convert async functions into promises (as in this thread) but I don't see how that can be applied to a recursive, async function. I guess I can just wrap the entire method as a Promise:
private func fileIdPromise(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
queryForFileId(query: query) { id, error in
if let error = error {
reject(error)
} else {
fulfill(id)
}
}
}
}
However, I was hoping to something a little more direct:
private func queryForFileId2(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
service.executeQuery(query) { ticket, data, error in
if let error = error {
reject(error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
// WHAT DO I DO HERE?
} else if let id = list.files?.first?.identifier {
fulfill(id)
} else {
fulfill(nil) // no file found
}
}
}
}
}
So: what would I do when I need to make another async call to executeQuery?
If you want to satisfy a recursive set of promises, at where your "WHAT DO I DO HERE?" line, you'd create a new promise.then {...}.else {...} pattern, calling fulfill in the then clause and reject in the else clause. Obviously, if no recursive call was needed, though, you'd just fulfill directly.
I don't know the Google API and you didn't share your code for satisfying a promise for a list of files, so I'll have to keep this answer a bit generic: Let's assume you had some retrieveTokens routine that returned a promise that is satisfied only when all of the promises for the all files was done. Let's imagine that the top level call was something like:
retrieveTokens(for: files).then { tokens in
print(tokens)
}.catch { error in
print(error)
}
You'd then have a retrieveTokens that returns a promise that is satisfied only when then promises for the individual files were satisfied. If you were dealing with a simple array of File objects, you might do something like:
func retrieveTokens(for files: [File]) -> Promise<[Any]> {
var fileGenerator = files.makeIterator()
let generator = AnyIterator<Promise<Any>> {
guard let file = fileGenerator.next() else { return nil }
return self.retrieveToken(for: file)
}
return when(fulfilled: generator, concurrently: 1)
}
(I know this isn't what yours looks like, but I need this framework to show my answer to your question below. But it’s useful to encapsulate this “return all promises at a given level” in a single function, as it allows you to keep the recursive code somewhat elegant, without repeating code.)
Then the routine that returns a promise for an individual file would see if a recursive set of promises needed to be returned, and put its fulfill inside the then clause of that new recursively created promise:
func retrieveToken(for file: File) -> Promise<Any> {
return Promise<Any> { fulfill, reject in
service.determineToken(for: file) { token, error in
// if any error, reject
guard let token = token, error == nil else {
reject(error ?? FileError.someError)
return
}
// if I don't have to make recursive call, `fulfill` immediately.
// in my example, I'm going to see if there are subfiles, and if not, `fulfill` immediately.
guard let subfiles = file.subfiles else {
fulfill(token)
return
}
// if I got here, there are subfiles and I'm going to start recursive set of promises
self.retrieveTokens(for: subfiles).then { tokens in
fulfill(tokens)
}.catch { error in
reject(error)
}
}
}
}
Again, I know that the above isn't a direct answer to your question (as I'm not familiar with Google Drive API nor how you did your top level promise logic). So, in my example, I created model objects sufficient for the purposes of the demonstration.
But hopefully it's enough to illustrate the idea behind a recursive set of promises.
I created simple project to check libraries like RxAlamofire and AlamofireObjectMapper. I have simple ApiService with one endpoint where is PHP script which works properly and returns JSON. I want call to recipeURL and I use flatMap operator to get response and provide it into Mapper where I should get Recipe object. How I can to do that?
Or is there other way?
class ApiService: ApiDelegate{
let recipeURL = "http://example.com/test/info.php"
func getRecipeDetails() -> Observable<Recipe> {
return request(.get, recipeURL)
.subscribeOn(MainScheduler.asyncInstance)
.observeOn(MainScheduler.instance)
.flatMap({ request -> Observable<Recipe> in
let json = ""//request.??????????? How to get JSON response?
guard let recipe: Recipe = Mapper<Recipe>().map(JSONObject: json) else {
return Observable.error(ApiError(message: "ObjectMapper can't mapping", code: 422))
}
return Observable.just(recipe)
})
}
}
From RxAlamofire's readme, it seems a method json(_:_:) exists in the library.
Typically, you'd rather use map instead of flatMap to transform the returned data to another format. flatMap would be useful if you needed to subscribe to a new observable (for example, doing a second request using part of the result from the first one).
return json(.get, recipeURL)
.map { json -> Recipe in
guard let recipe = Mapper<Recipe>().map(JSONObject: json) else {
throw ApiError(message: "ObjectMapper can't mapping", code: 422)
}
return recipe
}
I'm quite new and I'm wondering how to catch error from requests which are zipped (see snipped above) in one place. In current implementation I have error handling in two places, but my goal is to have it in one place. My requests are zipped because if one of this req gets failed whole sequence will fail so in result I want to have one error handling place in code for both request.
let firstReq = self.sendReq() // returns Observable<Bool>
.catchError {
error in
return self.just(true)
}
let secondReq = self.sendReqTwo() // returns Observable<Bool>
.catchError {
error in
return self.just(true)
}
goBttnOutlet.rx_tap
.subscribeNext {
Observable.zip(firstReqRes, secondReqRes) { (firstRes, secondRes) -> Bool in
return firstRes && secondRes
}.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(self.rx_disposableBag)
}.addDisposableTo(rx_disposableBag)
..maybe some link with example code with handling error in common place will be great for me.
Thanks a lot.
zip returns a new Observable<T>, so you can simply move the catchError operator application to what zip returns.
let firstReq = self.sendReq()
let secondReq = self.sendReqTwo()
let zippedReq = Observable.zip(firstReq, secondReq)
.catchErrorJustReturn { _ in true }
goBttnOutlet.rx_tap
.subscribeNext {
zippedReq.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(self.rx_disposableBag)
}.addDisposableTo(rx_disposableBag)
On a side note, you could improve the chain after goBttnOutlet to the following
goBttnOutlet.rx_tap.flatMap { zippedReq }
.subscribeNext { summaryRes in
print("🎿 \(summaryRes)")
}.addDisposableTo(rx_disposableBag)
See flatMap documentation for details.