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