Vapor 3 Beta Example Endpoint Request
Asked Answered
D

1

9

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())

}

}

Dobbs answered 2/2, 2018 at 19:12 Comment(3)
This is rather broad: "Please fill in this entire blank in my code". Which part, exactly, do you not know how to do? How is the documentation "unclear"?Woolridge
@Woolridge Hi Matt I tried to update my question to be more clear.Dobbs
great question @CoffeeAtBedtime. A super simple example would really help Tim's Ray Wenderlich book on Vapor 3 and also docs.vapor.codes/3.0/getting-started/asyncFigurine
B
22

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.

Benham answered 3/2, 2018 at 12:33 Comment(2)
Thank you so much this is exactly what I needed to break through the wall of understanding and I appreciate your throughout explanation.Dobbs
perfect answer @JoannisO. I had misunderstand the step: transforms the Future<Response> to Future<ExampleData>, until your example code.Figurine

© 2022 - 2024 — McMap. All rights reserved.