To read the data out of the csv file we’ll use almost the exact same approach as we used in the Netlify/PlanetScale workshop, so if you need a refresher those lessons are available here.
We’ll also go over it a bit now.
We’ll use cargo-edit to add the serde, csv, and color-eyre crates as dependencies of our upload-pokemon-data
package.
❯ cargo add -p upload-pokemon-data csv serde color-eyre
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding csv v1.1.6 to dependencies
Adding serde v1.0.136 to dependencies
Adding color-eyre v0.6.1 to dependencies
We’ll be using the derive
feature of the serde crate to generate the serialization boilerplate when turning a csv row into our Rust struct, PokemonCsv
. Make sure that the derive
feature is enabled in the upload-pokemon-data
Cargo.toml.
serde = { version = "1.0.136", features = ["derive"] }
Since we’re building what is more or less a one-off human-run script to upload data, we can take advantage of color-eyre
to handle returning different kinds of errors using ?
. The reasoning for this is that we don’t need to programmatically handle each different type of error, we’ll have a human watching this script run, so we can show the human the error and have them react to it.
use color_eyre::eyre;
use serde::{de, Deserialize};
fn main() -> eyre::Result<()> {
color_eyre::install()?;
let pokemon = csv::Reader::from_path(
"./crates/upload-pokemon-data/pokemon.csv",
)?
.deserialize()
.collect::<Result<Vec<PokemonCsv>, csv::Error>>()?;
dbg!(&pokemon[0]);
Ok(())
}
The return value of our main function needs to be eyre::Result
, which we see mirrored at the end where our final return value is a Result: Ok(())
.
We also need to install the reporting functionality using color_eyre::install()?
.
let pokemon = csv::Reader::from_path(
"./crates/upload-pokemon-data/pokemon.csv",
)?
.deserialize()
.collect::<Result<Vec<PokemonCsv>, csv::Error>>()?;
We can use the csv
crate to read rows from the csv file and deserialize those rows into a struct called PokemonCsv
that we’ll create later.
Whereas in other workshops we’ve used ?
to end the line we can also use it in a chain in the same way we use other functions.
csv::Reader::from_path
could fail, for example if the file we’ve specified doesn’t exist, so it returns a Result
. We then use ?
to return from main
if the value returned is an Err
, otherwise we pass the successful csv::Reader
value down the chain and call .deserialize
on the Reader
.
.deserialize
returns a struct that implements Iterator
, so we can .collect()
it into the container of our choice. Each item in this iterator is a Result
itself because every row could fail to deserialize into our PokemonCsv
.
In this case we expect no serialization errors from the csv, so we collect into a single Result
with a Vec<PokemonCsv>
as the success value. Effectively this is saying that if we have any error, return that first error, otherwise return the successful PokemonCsv
for each row.
dbg!(&pokemon[0]);
Because we’ve collected into a Vec<PokemonCsv>
. we can access the first pokemon at index 0 and print it out to the console and check it out.
The PokemonCsv
struct has the same structure as the one in the Netlify API workshop, and the CSV data is the same as well, so if you need a review of the PokemonCsv
struct, the from_comma_separated
or the from_capital_bool
functions, check out the relevant video here
#[derive(Debug, Deserialize, Clone)]
struct PokemonCsv {
name: String,
pokedex_id: u16,
#[serde(deserialize_with = "from_comma_separated")]
abilities: Vec<String>,
#[serde(deserialize_with = "from_comma_separated")]
typing: Vec<String>,
hp: u8,
attack: u8,
defense: u8,
special_attack: u8,
special_defense: u8,
speed: u8,
height: u16,
weight: u16,
generation: u8,
female_rate: Option<f32>,
#[serde(deserialize_with = "from_capital_bool")]
genderless: bool,
#[serde(
rename(deserialize = "legendary/mythical"),
deserialize_with = "from_capital_bool"
)]
is_legendary_or_mythical: bool,
#[serde(deserialize_with = "from_capital_bool")]
is_default: bool,
#[serde(deserialize_with = "from_capital_bool")]
forms_switchable: bool,
base_experience: u16,
capture_rate: u8,
#[serde(deserialize_with = "from_comma_separated")]
egg_groups: Vec<String>,
base_happiness: u8,
evolves_from: Option<String>,
primary_color: String,
number_pokemon_with_typing: f32,
normal_attack_effectiveness: f32,
fire_attack_effectiveness: f32,
water_attack_effectiveness: f32,
electric_attack_effectiveness: f32,
grass_attack_effectiveness: f32,
ice_attack_effectiveness: f32,
fighting_attack_effectiveness: f32,
poison_attack_effectiveness: f32,
ground_attack_effectiveness: f32,
fly_attack_effectiveness: f32,
psychic_attack_effectiveness: f32,
bug_attack_effectiveness: f32,
rock_attack_effectiveness: f32,
ghost_attack_effectiveness: f32,
dragon_attack_effectiveness: f32,
dark_attack_effectiveness: f32,
steel_attack_effectiveness: f32,
fairy_attack_effectiveness: f32,
}
fn from_capital_bool<'de, D>(
deserializer: D,
) -> Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str =
de::Deserialize::deserialize(deserializer)?;
match s {
"True" => Ok(true),
"False" => Ok(false),
_ => Err(de::Error::custom("not a boolean!")),
}
}
fn from_comma_separated<'de, D>(
deserializer: D,
) -> Result<Vec<String>, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str =
de::Deserialize::deserialize(deserializer)?;
Ok(s.split(", ")
.filter(|v| !v.is_empty())
.map(|v| v.to_string())
.collect())
}
With that code written, we can use cargo run --bin upload-pokemon-data
to see the first pokemon printed to the console.
[crates/upload-pokemon-data/src/main.rs:13] &pokemon[0] = PokemonCsv {
name: "Bulbasaur",
pokedex_id: 1,
abilities: [
"Overgrow",
"Chlorophyll",
],
typing: [
"Grass",
"Poison",
],
hp: 45,
attack: 49,
defense: 49,
special_attack: 65,
special_defense: 65,
speed: 45,
height: 7,
weight: 69,
generation: 1,
female_rate: Some(
0.125,
),
genderless: false,
is_legendary_or_mythical: false,
is_default: true,
forms_switchable: false,
base_experience: 64,
capture_rate: 45,
egg_groups: [
"Monster",
"Plant",
],
base_happiness: 70,
evolves_from: None,
primary_color: "green",
number_pokemon_with_typing: 15.0,
normal_attack_effectiveness: 1.0,
fire_attack_effectiveness: 2.0,
water_attack_effectiveness: 0.5,
electric_attack_effectiveness: 0.5,
grass_attack_effectiveness: 0.25,
ice_attack_effectiveness: 2.0,
fighting_attack_effectiveness: 0.5,
poison_attack_effectiveness: 1.0,
ground_attack_effectiveness: 1.0,
fly_attack_effectiveness: 2.0,
psychic_attack_effectiveness: 2.0,
bug_attack_effectiveness: 1.0,
rock_attack_effectiveness: 1.0,
ghost_attack_effectiveness: 1.0,
dragon_attack_effectiveness: 1.0,
dark_attack_effectiveness: 1.0,
steel_attack_effectiveness: 1.0,
fairy_attack_effectiveness: 0.5,
}