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
}