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
.
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.
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.
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.
#[tokio::test]
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.
DATABASE_URL=mysql://127.0.0.1 cargo test