Skip to content

Commit

Permalink
From URL paths to SQL queries
Browse files Browse the repository at this point in the history
With the ability to pass in arguments to our SQL query, we need a way to pass through arguments from the `event` our function receives as an argument to the SQL query. We're going to do that through the url path.

`event.path` gives us the url path for the request to our function. By default this will be something like `/.netlify/function/bulbasaur`. When we implement redirects to clean up our API routes later, that will become `/api/pokemon/bulbasaur`.

So to get the pokemon slug we should pass to the SQL query, we'll grab the last path segment in the URL path.

First we need to give a name to the event. We haven't changed the type, it is still an `ApiGatewayProxyReques`.

```rust
async fn handler(
    event: ApiGatewayProxyRequest,
    _: Context,
) -> Result<ApiGatewayProxyResponse, Error> {
```

We can then take the `event.path`, which according to the `ApiGatewayProxyRequest` type, might not exist.

`path.split("/")` will give us an iterator over the path segments. Notably the beginning and end of the iterator can be `""` if it starts or ends with a `/`.

`last` will consume the entire iterator until it produces its last value, which in this case is the last path segment.

```rust
let path = event
    .path
    .expect("expect there to always be an event path");
let requested_pokemon = path.split("/").last();
```

We can `match` on `requested_pokemon` to handle any errors. `Some("")` allows us to match on potentially empty values, for example when someone sends a request with a trailing slash, or if the final path segment is an empty string.

`None` is actually a hard error for us. It means that `path.split("/")` is an empty iterator, because otherwise `last()` will return `Some`. Since `path.split("/")` even on an empty string will result in `Some("")`, we can fail hard here because we expect `None` to never happen.

Finally we have the success case, where a `pokemon_name` was successfully retrieved from the path. This code is the same code we had before for our `handler`, with the addition of using `pokemon_name` instead of a hardcoded string.

```rust
match requested_pokemon {
    Some("") => todo!(),
    None => todo!(),
    Some(pokemon_name) => {
        let pool = MySqlPoolOptions::new()
            .max_connections(5)
            .connect(&database_url)
            .await?;
        let result = sqlx::query_as!(
                PokemonHp,
                r#"SELECT name, hp FROM pokemon WHERE slug = ?"#,
                pokemon_name
            )
            .fetch_one(&pool)
            .await?;

        let json_pokemon =
            serde_json::to_string(&result)?;
        let response = ApiGatewayProxyResponse {
            status_code: 200,
            headers: HeaderMap::new(),
            multi_value_headers: HeaderMap::new(),
            body: Some(Body::Text(json_pokemon)),
            is_base64_encoded: Some(false),
        };
        Ok(response)
    }
}
```

We need to update our test to account for the new logic. Our fake `event` will have a `path` field that will have a Pokemon slug at the end of the segments.

This will now fetch `bulbasaur` from the database.

```rust
async fn handler_handles() {
    let event = ApiGatewayProxyRequest {
        resource: None,
        path: Some(
            "/api/pokemon/bulbasaur".to_string(),
        ),
        ...
    };

    assert_eq!(
        handler(event.clone(), Context::default())
            .await
            .unwrap(),
        ApiGatewayProxyResponse {
            status_code: 200,
            headers: HeaderMap::new(),
            multi_value_headers: HeaderMap::new(),
            body: Some(Body::Text(
                serde_json::to_string(&PokemonHp {
                    name: String::from("Bulbasaur"),
                    hp: 45
                },)
                .unwrap()
            )),
            is_base64_encoded: Some(false),
        }
    )
}
```

Running cargo test with the database url will now pass.

```rust
DATABASE_URL=mysql://127.0.0.1 cargo test
```
  • Loading branch information
ChristopherBiscardi committed Oct 17, 2021
1 parent 9cc4ab9 commit 0d65556
Showing 1 changed file with 35 additions and 27 deletions.
62 changes: 35 additions & 27 deletions crates/pokemon-api/src/main.rs
Expand Up @@ -25,34 +25,43 @@ struct PokemonHp {
}

async fn handler(
_: ApiGatewayProxyRequest,
event: ApiGatewayProxyRequest,
_: Context,
) -> Result<ApiGatewayProxyResponse, Error> {
println!("handler");
let database_url = env::var("DATABASE_URL")?;
let path = event
.path
.expect("expect there to always be an event path");
let requested_pokemon = path.split("/").last();
match requested_pokemon {
Some("") => todo!(),
None => todo!(),
Some(pokemon_name) => {
let pool = MySqlPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
let result = sqlx::query_as!(
PokemonHp,
r#"SELECT name, hp FROM pokemon WHERE slug = ?"#,
pokemon_name
)
.fetch_one(&pool)
.await?;

let pool = MySqlPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;

let result = sqlx::query_as!(
PokemonHp,
r#"SELECT name, hp FROM pokemon WHERE slug = ?"#,
"charmander"
)
.fetch_one(&pool)
.await?;

let json_pokemon = serde_json::to_string(&result)?;
let response = ApiGatewayProxyResponse {
status_code: 200,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text(json_pokemon)),
is_base64_encoded: Some(false),
};
Ok(response)
let json_pokemon =
serde_json::to_string(&result)?;
let response = ApiGatewayProxyResponse {
status_code: 200,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text(json_pokemon)),
is_base64_encoded: Some(false),
};
Ok(response)
}
}
}

#[cfg(test)]
Expand All @@ -72,8 +81,7 @@ mod tests {
let event = ApiGatewayProxyRequest {
resource: None,
path: Some(
"/.netlify/functions/pokemon-api"
.to_string(),
"/api/pokemon/bulbasaur".to_string(),
),
http_method: Method::GET,
headers: HeaderMap::new(),
Expand Down Expand Up @@ -130,8 +138,8 @@ mod tests {
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text(
serde_json::to_string(&PokemonHp {
name: String::from("Charmander"),
hp: 39
name: String::from("Bulbasaur"),
hp: 45
},)
.unwrap()
)),
Expand Down

0 comments on commit 0d65556

Please sign in to comment.