We now have a table set up that we can insert PokemonCsvRow
s into, but the Iterator returned by deserialize()
in src/main.rs
only gives us PokemonCsv
s.
Our goal is to get this line of code working in src/main.rs
.
let pokemon_row: PokemonTableRow = record.into();
This line of code specifies that the variable pokemon_row
is a PokemonTableRow
, which into()
can then use to convert the record
, a PokemonCsv
, into a PokemonTableRow
.
into
is not magic: we, the standard library, or someone else have to define how it works for these two types.
The into
function comes from the Into
trait so it would be reasonable to think that we could implement the Into
trait for impl Into<PokemonTableRow> for PokemonCsv
. We definitely could do this and it would work but there's a more idiomatic trait to implement: From
.
From
and Into
do essentially the same thing. The reason both exist is partially historical and partially to support edge cases. There are (or have been in the past) types we could not implement From
for. We talked previously about the orphan rules (that is, implementing a trait for types that aren't owned by our current crate), and this is one of the ways that From
used to be more restrictive than Into
. This is OK. It means Rust used to be more strict and improvements were made and it got easier to use.
As a result of this, any type that implements From
also gets an Into
implementation for free. (It does not work the other way around).
So if we implement impl From<PokemonCsv> for PokemonTableRow
, then we get pokemon_csv.into()
for free.
We'll implement From
in src/db.rs
since that's where PokemonTableRow
is. The From
trait requires us to implement the from
function which takes a PokemonCsv
and returns Self
, a PokemonTableRow
.
Since we're using PokemonCsv
we'll bring it into scope at the top of the file.
use crate::pokemon_csv::PokemonCsv;
We'll destructure the PokemonCsv
in the from
argument. This brings each of the names for each of the fields from PokemonCsv
into scope. We use _
in the value place of the destrucuring for fields that we don't use.
impl From<PokemonCsv> for PokemonTableRow {
fn from(
PokemonCsv {
name,
pokedex_id,
abilities: _,
typing: _,
hp,
attack,
defense,
special_attack,
special_defense,
speed,
height,
weight,
generation,
female_rate,
genderless,
is_legendary_or_mythical,
is_default,
forms_switchable,
base_experience,
capture_rate,
egg_groups: _,
base_happiness,
evolves_from: _,
primary_color,
number_pokemon_with_typing,
normal_attack_effectiveness,
fire_attack_effectiveness,
water_attack_effectiveness,
electric_attack_effectiveness,
grass_attack_effectiveness,
ice_attack_effectiveness,
fighting_attack_effectiveness,
poison_attack_effectiveness,
ground_attack_effectiveness,
fly_attack_effectiveness,
psychic_attack_effectiveness,
bug_attack_effectiveness,
rock_attack_effectiveness,
ghost_attack_effectiveness,
dragon_attack_effectiveness,
dark_attack_effectiveness,
steel_attack_effectiveness,
fairy_attack_effectiveness,
}: PokemonCsv,
) -> Self {
let id = PokemonId::new();
let slug = name.to_kebab_case();
PokemonTableRow {
id,
slug,
name,
pokedex_id,
hp: hp.into(),
attack: attack.into(),
defense: defense.into(),
special_attack: special_attack.into(),
special_defense: special_defense.into(),
speed: speed.into(),
height,
weight,
generation: generation.into(),
female_rate,
genderless,
legendary_or_mythical: is_legendary_or_mythical,
is_default,
forms_switchable,
base_experience,
capture_rate: capture_rate.into(),
base_happiness: base_happiness.into(),
primary_color,
number_pokemon_with_typing,
normal_attack_effectiveness,
fire_attack_effectiveness,
water_attack_effectiveness,
electric_attack_effectiveness,
grass_attack_effectiveness,
ice_attack_effectiveness,
fighting_attack_effectiveness,
poison_attack_effectiveness,
ground_attack_effectiveness,
fly_attack_effectiveness,
psychic_attack_effectiveness,
bug_attack_effectiveness,
rock_attack_effectiveness,
ghost_attack_effectiveness,
dragon_attack_effectiveness,
dark_attack_effectiveness,
steel_attack_effectiveness,
fairy_attack_effectiveness,
}
}
}
We can construct a new PokemonId
using the new
method we've defined for it. We'll use that as the id
field to our PokemonTableRow
.
let id = PokemonId::new();
To convert our pokemon name
s into slugs, we can install the inflector
crate. When we do, cargo tells us that Inflector
was added instead of inflector
. This is because the Inflector
package was published originally with a capital I
, but also that crate names are case-insensitive, so inflector
and Inflector
are the same crate anyway, always. So cargo gives us the one we meant.
❯ cargo add inflector -p upload-pokemon-data
WARN: Added `Inflector` instead of `inflector`
Updating '<https://github.com/rust-lang/crates.io-index>' index
Adding Inflector v0.11.4 to dependencies
We'll need to bring the Inflector
trait into scope, which is an extension trait that adds additional methods to strings.
use inflector::Inflector;
One of those methods is to_kebab_case
, which is good enough for us to slugify our names.
let slug = name.to_kebab_case();
Finally we construct a PokemonTableRow
. Since we have names in scope that are the same as the names in the PokemonTableRow
type, we can write them once instead of writing the field name and the value. The one exception is the places that we're converting u8
s to u16
s using into
.
Rust knows how to convert the u8
s into u16
s because each struct's field is typed as such and the relevant traits are implemented for many standard library primitive types.
Our main
function in src/main.rs
can now change to use into
. We'll also change the println
to show pokemon_row
.
fn main() -> Result<(), csv::Error> {
let mut rdr = csv::Reader::from_path(
"./crates/upload-pokemon-data/pokemon.csv",
)?;
for result in rdr.deserialize() {
let record: PokemonCsv = result?;
let pokemon_row: PokemonTableRow = record.into();
dbg!(pokemon_row);
}
Ok(())
}
and a cargo run
should show us a bunch of PokemonTableRow
s with the ids and slugs we've generated.
[crates/upload-pokemon-data/src/main.rs:13] pokemon_row = PokemonTableRow {
id: PokemonId(
"2VYh2bIMdAgd4oIPsdBFSAXmc35",
),
name: "Calyrex Shadow Rider",
slug: "calyrex-shadow-rider",
pokedex_id: 898,
hp: 100,
attack: 85,
defense: 80,
special_attack: 165,
special_defense: 100,
speed: 150,
height: 24,
weight: 536,
generation: 8,
female_rate: None,
genderless: true,
legendary_or_mythical: true,
is_default: false,
forms_switchable: true,
base_experience: 340,
capture_rate: 3,
base_happiness: 100,
primary_color: "green",
number_pokemon_with_typing: 4.0,
normal_attack_effectiveness: 0.0,
fire_attack_effectiveness: 1.0,
water_attack_effectiveness: 1.0,
electric_attack_effectiveness: 1.0,
grass_attack_effectiveness: 1.0,
ice_attack_effectiveness: 1.0,
fighting_attack_effectiveness: 0.0,
poison_attack_effectiveness: 0.5,
ground_attack_effectiveness: 1.0,
fly_attack_effectiveness: 1.0,
psychic_attack_effectiveness: 0.5,
bug_attack_effectiveness: 1.0,
rock_attack_effectiveness: 1.0,
ghost_attack_effectiveness: 4.0,
dragon_attack_effectiveness: 1.0,
dark_attack_effectiveness: 4.0,
steel_attack_effectiveness: 1.0,
fairy_attack_effectiveness: 1.0,
}