Example code
I strongly recommend you read the explaination below, but this is the code.
struct ExampleData: Codable {
var array : [Int]
var dict : [String : String]
}
// Register a GET /example route
router.get("example") { req -> Future<View> in
// Fetch an HTTP Client instance
let client = try req.make(Client.self)
// Send an HTTP Request to example.vapor.codes/json over plaintext HTTP
// Returns `Future<Response>`
let response = client.get("http://example.vapor.codes/json")
// Transforms the `Future<Response>` to `Future<ExampleData>`
let exampleData = response.flatMap(to: ExampleData.self) { response in
return response.content.decode(ExampleData.self)
}
// Renders the `ExampleData` into a `View`
return try req.view().render("home", exampleData)
}
Futures
A Future<Expectation>
is a wrapper around the Expectation
. The expectation can be successful or failed (with an Error).
The Future type can register callbacks which are executed on successful completion. One of these callbacks that we use here is flatMap
. Let's dive into a regular map
, first.
If you map
a Future you transform the future's successful Expectation
and transparently pass through error conditions.
let promise = Promise<String>()
let stringFuture = promise.future // Future<String>
let intFuture = stringFuture.map(to: Int.self) { string -> Int in
struct InvalidNumericString: Error {}
guard let int = Int(string) else { throw InvalidNumericString() }
return int // Int
}
intFuture.do { int in
print("integer: ", int)
}.catch { error in
print("error: \(error)")
}
If we complete the promise with a valid decimal integer formatted string like "4"
it'll print integer: 4
promise.complete("4")
If we place any non-numeric characters in there like "abc"
it'll throw an error inside the InvalidNumericString
error which will be triggering the catch
block.
promise.complete("abc")
No matter what you do, an error thrown from a map
or flatMap
function will cascade transparently through other transformations. Transforming a future will transform the Expectation
only, and only be triggered on successful cases. Error cases will be copied from the "base future" to the newly transformed future.
If instead of completing the promise you fail the promise, the map
block will never be triggered and the AnyError
condition will be found in the catch
block instead.
struct AnyError: Error {}
promise.fail(AnyError())
flatMap
works very similarly to the above example. It's a map
where the trailing closure returns a Future<Expectation>
rather than Expectation
.
So If we'd rewrite the map block to be a flatMap
, although impractical, we'll end up with this:
let intFuture = stringFuture.flatMap(to: Int.self) { string -> Future<Int> in
struct InvalidNumericString: Error {}
guard let int = Int(string) else { throw InvalidNumericString() }
return Future(int) // Int
}
intFuture
is still a Future<Int>
because the recursive futures will be flattened from Future<Future<Int>>
to just Future<Int>
.
Content
The response.content.decode
bit reads the Content-Type and looks for the default Decoder
for this Content Type. The decoded struct will then be returned as a Future<DecodedStruct>
, in this case this struct is ExampleData
.
The reason the content is returned asynchronously is because the content may not have completely arrived in the HTTP response yet. This is a necessary abstraction because we may be receiving files upwards of 100MB which could crash (cloud) servers with a small amount of memory available.
Logic
Back to the original route:
- First make a client
- Make a request to
http://example.vapor.codes/json
- Read the content from the
Future<Response>
asynchronously
- Render the results into the view asynchronously
- Return the
Future<View>
The framework will understand that you're returning a Future<View>
and will continue processing other requests rather than waiting on the results.
Once the JSON is received, this request will be picked up again and processed into a response which your web browser will receive.
Leaf is built on top of TemplateKit which will await the future asynchronously. Just like Vapor, Leaf and TemplateKit will understand Futures well enough that you can pass a Future instead of a struct (or vice versa) and they'll switch to anothe request until the future is completed, if necessary.