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.
error[E0277]: the trait bound `PokemonId: sqlx::Decode<'_, MySql>` is not satisfied
--> crates/pokemon-api/src/main.rs:46:26
|
46 | let result = sqlx::query_as!(
| __________________________^
47 | | PokemonHp,
48 | | r#"
49 | | SELECT
... |
59 | | pokemon_name
60 | | )
| |_____________^ the trait `sqlx::Decode<'_, MySql>` is not implemented for `PokemonId`
|
= help: the following other types implement trait `sqlx::Decode<'r, DB>`:
<bool as sqlx::Decode<'r, sqlx::Any>>
<bool as sqlx::Decode<'_, MySql>>
<i8 as sqlx::Decode<'_, MySql>>
<i16 as sqlx::Decode<'r, sqlx::Any>>
<i16 as sqlx::Decode<'_, MySql>>
<i32 as sqlx::Decode<'r, sqlx::Any>>
<i32 as sqlx::Decode<'_, MySql>>
<i64 as sqlx::Decode<'r, sqlx::Any>>
and 22 others
note: required by a bound in `try_get_unchecked`
--> /Users/chris/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sqlx-core-0.7.1/src/row.rs:156:12
|
153 | fn try_get_unchecked<'r, T, I>(&'r self, index: I) -> Res...
| ----------------- required by a bound in this associated function
...
156 | T: Decode<'r, Self::Database>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Row::try_get_unchecked`
= note: this error originates in the macro `$crate::sqlx_macros::expand_query` which comes from the expansion of the macro `sqlx::query_as` (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:15:32
|
15 | #[derive(Debug, sqlx::FromRow, Serialize)]
| ^^^^^^^^^ the trait `Serialize` is not implemented for `PokemonId`
16 | struct PokemonHp {
17 | id: PokemonId,
| -- required by a bound introduced by this call
|
= help: the following other types implement trait `Serialize`:
bool
char
isize
i8
i16
i32
i64
i128
and 190 others
note: required by a bound in `_serde::ser::SerializeStruct::serialize_field`
--> /Users/chris/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-1.0.188/src/ser/mod.rs:1865:12
|
1859 | fn serialize_field<T: ?Sized>(
| --------------- required by a bound in this associated function
...
1865 | T: Serialize;
| ^^^^^^^^^ required by this bound in `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.
In upload-pokemon-data/src/db.rs, we'll need to 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.
Back in the main.rs of pokemon-api we still need to update our test. Your ksuid will be different than mine, so keep that in mind.
assert_eq!(
response.body(),
&Body::Text(
"{\"id\":\"2Vbj42oybesL4UeqponMuj2IPxt\",\"name\":\"Bulbasaur\",\"hp\":45,\"legendary_or_mythical\":false}"
.to_string()
)
);
The final API response should look like this:
{
"id": "1xu3YJ9hUblDpSoqRXwgXx92V9a",
"name": "Zapdos",
"hp": 90,
"legendary_or_mythical": true
}