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.
To get the pokemon slug we should pass to the SQL query, we'll grab the last path segment in the URL path.
Updating Fixtures
The very first action I want to take here is to fix the path in the test payload we use when we invoke our function.
Add a dbg! to the function to be able to view the incoming path from the request.
async fn function_handler(
event: Request,
) -> Result<Response<Body>, Error> {
dbg!(event.uri().path());
...
There are three commands that need to be run. I'll list them all here on their own line.
pscale connect pokemon main
DATABASE_URL=mysql://127.0.0.1:3306 cargo lambda watch
cargo lambda invoke pokemon-api --data-example apigw-request
After invoking, our function logs look like this:
❯ DATABASE_URL=mysql://127.0.0.1:3306 cargo lambda watch
INFO invoke server listening on [::]:9000
INFO starting lambda function function="pokemon-api" manifest="Cargo.toml"
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/pokemon-api`
[crates/pokemon-api/src/main.rs:18] event.uri().path() = "/testStage/hello/world"
The path is /testStage/hello/world, which doesn't help us much.
We can pass in any arbitrary data when invoking if we want to, but the JSON we pass in has to deserialize into the proper type. An example that does not deserialize into a LambdaRequest is shown here. The LambdaRequest is an internal type in the lambda_http crate, but you can see it here if you want to.
❯ cargo lambda invoke pokemon-api --data-ascii "{ \"command\": \"hi\" }"
Error: lambda_runtime::deserializer::DeserializeError
× failed to deserialize the incoming data into the function's
│ payload type: data did not match any variant of untagged enum
│ LambdaRequest
│
Was this error unexpected?
Open an issue in https://github.com/cargo-lambda/cargo-lambda/
issues
So let's go into our apigw-request.json file and change the path.
"path": "/bulbasaur",
If we use that file using the --data-file flag, we'll now see the path in our function.
❯ cargo lambda invoke pokemon-api --data-file ./crates/pokemon-api/src/apigw-request.json
[crates/pokemon-api/src/main.rs:18] event.uri().path() = "/testStage/bulbasaur"
parsing the path
path.split("/") will give us an iterator over the path segments. Notably one issue with this is that 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.uri().path();
let requested_pokemon = path.split("/").last();
matching on the requested_pokemon
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. path.split("/") even on an empty string will result in an iterator over the equivalent of vec![""]. This means 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 function_handler, with the addition of using pokemon_name instead of a hardcoded string.
match requested_pokemon {
None => todo!("this is a hard error, return 500"),
Some("") => todo!("we can not find a pokemon without a name, return 400"),
Some(pokemon_name) => {
let database_url = env::var("DATABASE_URL")?;
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 pokemon = serde_json::to_string(&result)?;
let resp = Response::builder()
.status(200)
.header(CONTENT_TYPE, "application/json")
.body(Body::Text(pokemon))?;
Ok(resp)
}
}
invoke will now fetch bulbasaur from the database if we use our newly modified request fixture.
cargo lambda invoke pokemon-api --data-file ./crates/pokemon-api/src/apigw-request.json
{
"statusCode": 200,
"headers":
{
"content-type": "application/json"
},
"multiValueHeaders":
{
"content-type":
[
"application/json"
]
},
"body": "{\"name\":\"Bulbasaur\",\"hp\":45}",
"isBase64Encoded": false
}
Testing
Our test also now fails because we changed the fixture to use another pokemon
---- tests::accepts_apigw_request stdout ----
thread 'tests::accepts_apigw_request' panicked at 'assertion failed: `(left == right)`
left: `Text("{\"name\":\"Bulbasaur\",\"hp\":45}")`,
right: `Text("{\"name\":\"Charmander\",\"hp\":39}")`', crates/pokemon-api/src/main.rs:70:9
In our test we need to use the pokemon we're using in our fixture for the test to pass.
"{\"name\":\"Bulbasaur\",\"hp\":45}"
Running cargo test with the database url will now pass.
DATABASE_URL=mysql://127.0.0.1 cargo test