user3612973
user3612973

Reputation:

Vapor 3 Beta Example Endpoint Request

I am trying to find a simple example of how inside a router a person would send a request to the vapor sample endpoint http://example.vapor.codes/json, receive a response and map it to a struct or class.

I've seen examples elsewhere for Vapor 2 but they are no longer relevant with Vapor 3 and the current Vapor 3 beta documentation isn't clear.

Something like...

router.get("sample") { req in

  //1. create client
  //2. send get request to sample endpoint at http://example.vapor.codes/json
  //3. handle response and map to a struct or class

}

My goal is to go grab something off the endpoint, turn it into a struct or class and display it in a leaf view.

{"array":[0,1,2,3],"dict":{"lang":"Swift","name":"Vapor"},"number":123,"string":"test"}

Here is my outline for how I think it is done but I don't understand how to handle the response and process into the struct so that I can use it in my home.leaf in its html (I'm not concerned with the leaf part assume I have all the configuration for all that and imports already).

router.get("example"){ req -> Future<View> in

    struct ExampleData: Codable {
        var array : [Int]
        var dict : [String : String]
    }

    return try req.make(Client.self).get("http://example.vapor.codes/json").flatMap(to: ExampleData.self) { res in
        //not sure what to do to set the values of the ExampleData
    }

    return try req.view().render("home", ExampleData())

}

}

Upvotes: 9

Views: 3190

Answers (1)

JoannisO
JoannisO

Reputation: 930

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.

Upvotes: 22

Related Questions