We have to do something similar to what we did for the boolean field, for the id field.
We'll start off in main.rs inside of pokemon-api.
First pull PokemonId from upload_pokemon_data, our other crate, into scope. We haven't exposed this from the crate yet but we will soon.
use upload_pokemon_data::PokemonId;
Then we'll update PokemonHp to include the id.
#[derive(Debug, sqlx::FromRow, Serialize)]
struct PokemonHp {
id: PokemonId,
name: String,
hp: u16,
legendary_or_mythical: bool,
}
and our SQL query to include fetching the id as a PokemonId.
SELECT
id as "id!: PokemonId",
name,
hp,
legendary_or_mythical as "legendary_or_mythical!: bool"
FROM
pokemon
WHERE
slug = ?
We have to add upload-pokemon-data in our pokemon-api/Cargo.toml to be able to access PokemonId. To do that we'll use the path field for specifying dependencies in our dependencies array.
upload-pokemon-data = { path = "../upload-pokemon-data" }
This will point to the folder containing the upload-pokemon-data crate. Unfortunately we haven't specified a library crate for upload-pokemon-data, so this won't compile yet.
Next to upload-pokemon-data/main.rs we'll create another entrypoint lib.rs. lib.rs is the default entrypoint for a Rust library crate that we can use as a dependency in other crates. The main.rs remains our binary entrypoint, which other crates won't be able to access.
Inside of lib.rs, we need to declare the same modules we do in main.rs. We will also take advantage of the pub keyword as we bring PokemonId into the top-level scope for the crate to make the PokemonId publicly available to other crates to use.
mod db;
mod pokemon_csv;
pub use db::PokemonId;
This leaves us with the real work if we try to cargo test. We have to implement sqlx::Decode and Serialize for PokemonId.
--> crates/pokemon-api/src/main.rs:68:26
|
68 | let result = sqlx::query_as!(
| __________________________^
69 | | PokemonHp,
70 | | r#"
71 | | SELECT
... |
81 | | pokemon_name
82 | | )
| |_________________^ the trait `sqlx::Decode<'_, MySql>` is not implemented for `PokemonId`
|
= note: this error originates in the macro `$crate::sqlx_macros::expand_query` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `PokemonId: Serialize` is not satisfied
--> crates/pokemon-api/src/main.rs:35:5
|
35 | id: PokemonId,
| ^^ the trait `Serialize` is not implemented for `PokemonId`
|
note: required by `_serde::ser::SerializeStruct::serialize_field`
The Decode implementation (which we'll put in db.rs) uses lifetimes explicitly and similarly to the way we wrote Encode for PokemonId, these types come directly from sqlx so there's not much to do but copy the sqlx type signatures and drop MySql in because we're only querying against MySQL.
You'll need bring some additional types in like Decode and HasValueRef from sqlx
use sqlx::{
database::{HasArguments, HasValueRef},
encode::IsNull,
mysql::MySqlTypeInfo,
Database, Decode, Encode, MySql, MySqlPool, Type,
};
The implementation reads: for some lifetime 'r, we'll implement the Decode trait with respect to 'r and MySql for PokemonId.
In plain english, we're writing an implementation of Decode that can decode a PokemonId from a MySql query field's value.
The decode function that we have to implement takes a value, which is a MySql. We're taking ValueRef on the MySql, while ensuring that the implementation used to get the ValueRef is the HasValueRef trait implementation's.
The function returns a result because it could fail.
impl<'r> Decode<'r, MySql> for PokemonId
{
fn decode(
value: <MySql as HasValueRef<'r>>::ValueRef,
) -> Result<
PokemonId,
Box<dyn std::error::Error + 'static + Send + Sync>,
> {
let value =
<&[u8] as Decode<MySql>>::decode(value)?;
let base62_ksuid = std::str::from_utf8(&value)?;
Ok(PokemonId(Ksuid::from_base62(
base62_ksuid,
)?))
}
}
When we decode the shared reference to a u8 slice &[u8], we make sure that the decode function we're using comes from the Decode trait on the MySql type.
Then we take the [u8] we've decoded and use the std::str::from_utf8 function to turn it back into a base62 formatted Ksuid. We worked with base62 Ksuids in the uploading data workshop.
Finally, we need to implement Serialize for PokemonId so that we can serialize the value when creating our JSON response.
The type signature here again comes straight from serde. We don't get a choice. So that leaves only the implementation up to us.
impl Serialize for PokemonId {
fn serialize<S>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let id = self.0.to_base62();
serializer.serialize_str(&id)
}
}
First we turn the Ksuid into a base62 string, then we call out to the serializer serde gave us in the function argument which helpfully has functions to serialize all of Rust's primitives for us already. So in effect we don't have to write any serialization code, we only have to turn our PokemonId into a string.
The final API response should look like this:
{
"id": "1xu3YJ9hUblDpSoqRXwgXx92V9a",
"name": "Zapdos",
"hp": 90,
"legendary_or_mythical": true
}