use std::collections::HashMap;
use crate::models::{EvolutionTree, PokemonDetail, PokemonEntry, Sprite, Stat, StatKind};
const BASE_URL: &str = "https://pokeapi.co/api/v2";
const LIST_LIMIT: u32 = 1302;
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("network request failed: {0}")]
Network(#[from] reqwest::Error),
#[error("could not locate the evolution chain for this Pokemon")]
MissingEvolutionChain,
#[error("could not decode sprite image: {0}")]
Image(#[from] image::ImageError),
}
pub fn build_client() -> Result<reqwest::Client, ApiError> {
let client = reqwest::Client::builder()
.user_agent(concat!("pokeductor/", env!("CARGO_PKG_VERSION")))
.build()?;
Ok(client)
}
pub async fn fetch_pokemon_list(
client: &reqwest::Client,
) -> Result<Vec<PokemonEntry>, ApiError> {
let url = format!("{BASE_URL}/pokemon?limit={LIST_LIMIT}&offset=0");
let raw: NamedList = client.get(url).send().await?.error_for_status()?.json().await?;
let entries = raw
.results
.into_iter()
.map(|r| PokemonEntry { name: r.name })
.collect();
Ok(entries)
}
pub async fn fetch_pokemon_bundle(
client: &reqwest::Client,
name: &str,
) -> Result<(PokemonDetail, EvolutionTree, Option<Sprite>), ApiError> {
let mut detail = fetch_detail(client, name).await?;
let species = fetch_species(client, &detail.species).await?;
detail.dex_number = species.dex_number;
detail.is_legendary = species.is_legendary;
detail.is_mythical = species.is_mythical;
detail.is_baby = species.is_baby;
detail.genera = species.genera;
detail.flavors = species.flavors;
let evolution = fetch_chain(client, &species.chain_url).await?;
let sprite = match &detail.sprite_url {
Some(url) => fetch_sprite(client, url).await.ok(),
None => None,
};
Ok((detail, evolution, sprite))
}
pub async fn translate_text(
client: &reqwest::Client,
text: &str,
from: &str,
to: &str,
) -> Result<String, ApiError> {
let clamped: String = text.chars().take(500).collect();
let pair = format!("{from}|{to}");
let resp: MyMemoryResponse = client
.get("https://api.mymemory.translated.net/get")
.query(&[("q", clamped.as_str()), ("langpair", pair.as_str())])
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp.response_data.translated_text)
}
pub async fn fetch_named_sprite(
client: &reqwest::Client,
name: &str,
) -> Result<Option<Sprite>, ApiError> {
let detail = fetch_detail(client, name).await?;
match detail.sprite_url {
Some(url) => Ok(Some(fetch_sprite(client, &url).await?)),
None => Ok(None),
}
}
async fn fetch_sprite(client: &reqwest::Client, url: &str) -> Result<Sprite, ApiError> {
let bytes = client.get(url).send().await?.error_for_status()?.bytes().await?;
let image = image::load_from_memory(&bytes)?.to_rgba8();
let (width, height) = image.dimensions();
let pixels = image.pixels().map(|p| p.0).collect();
Ok(Sprite { width, height, pixels })
}
async fn fetch_detail(client: &reqwest::Client, name: &str) -> Result<PokemonDetail, ApiError> {
let url = format!("{BASE_URL}/pokemon/{name}");
let raw: RawPokemon = client.get(url).send().await?.error_for_status()?.json().await?;
let mut types: Vec<(u8, String)> = raw
.types
.into_iter()
.map(|t| (t.slot, t.type_.name))
.collect();
types.sort_by_key(|(slot, _)| *slot);
let mut stats: Vec<Stat> = raw
.stats
.into_iter()
.filter_map(|s| {
StatKind::from_api(&s.stat.name).map(|kind| Stat {
kind,
base: s.base_stat,
})
})
.collect();
stats.sort_by_key(|s| s.kind.order());
Ok(PokemonDetail {
name: raw.name,
species: raw.species.name,
dex_number: raw.id,
is_legendary: false,
is_mythical: false,
is_baby: false,
types: types.into_iter().map(|(_, name)| name).collect(),
stats,
height: raw.height,
weight: raw.weight,
sprite_url: raw.sprites.front_default,
genera: HashMap::new(),
flavors: HashMap::new(),
})
}
const CARD_LANGS: [&str; 5] = ["en", "de", "fr", "es", "it"];
struct SpeciesInfo {
chain_url: String,
dex_number: u32,
is_legendary: bool,
is_mythical: bool,
is_baby: bool,
genera: HashMap<String, String>,
flavors: HashMap<String, String>,
}
async fn fetch_species(client: &reqwest::Client, name: &str) -> Result<SpeciesInfo, ApiError> {
let url = format!("{BASE_URL}/pokemon-species/{name}");
let species: RawSpecies = client.get(url).send().await?.error_for_status()?.json().await?;
let chain_url = species
.evolution_chain
.map(|c| c.url)
.ok_or(ApiError::MissingEvolutionChain)?;
let mut genera = HashMap::new();
for g in &species.genera {
if CARD_LANGS.contains(&g.language.name.as_str()) {
genera.entry(g.language.name.clone()).or_insert_with(|| g.genus.clone());
}
}
let mut flavors = HashMap::new();
for e in &species.flavor_text_entries {
if CARD_LANGS.contains(&e.language.name.as_str()) {
flavors
.entry(e.language.name.clone())
.or_insert_with(|| clean_flavor(&e.flavor_text));
}
}
Ok(SpeciesInfo {
chain_url,
dex_number: species.id,
is_legendary: species.is_legendary,
is_mythical: species.is_mythical,
is_baby: species.is_baby,
genera,
flavors,
})
}
async fn fetch_chain(client: &reqwest::Client, url: &str) -> Result<EvolutionTree, ApiError> {
let chain: RawEvolutionChain = client.get(url).send().await?.error_for_status()?.json().await?;
Ok(parse_chain(&chain.chain))
}
fn clean_flavor(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn parse_chain(link: &RawChainLink) -> EvolutionTree {
EvolutionTree {
name: link.species.name.clone(),
children: link.evolves_to.iter().map(parse_chain).collect(),
}
}
#[derive(serde::Deserialize)]
struct NamedList {
results: Vec<NamedResource>,
}
#[derive(serde::Deserialize)]
struct NamedResource {
name: String,
#[allow(dead_code)]
#[serde(default)]
url: String,
}
#[derive(serde::Deserialize)]
struct RawPokemon {
id: u32,
name: String,
height: u32,
weight: u32,
types: Vec<RawTypeSlot>,
stats: Vec<RawStatSlot>,
sprites: RawSprites,
species: NamedResource,
}
#[derive(serde::Deserialize)]
struct RawSprites {
#[serde(default)]
front_default: Option<String>,
}
#[derive(serde::Deserialize)]
struct RawTypeSlot {
slot: u8,
#[serde(rename = "type")]
type_: NamedResource,
}
#[derive(serde::Deserialize)]
struct RawStatSlot {
base_stat: u16,
stat: NamedResource,
}
#[derive(serde::Deserialize)]
struct RawSpecies {
id: u32,
#[serde(default)]
is_legendary: bool,
#[serde(default)]
is_mythical: bool,
#[serde(default)]
is_baby: bool,
evolution_chain: Option<RawChainRef>,
#[serde(default)]
genera: Vec<RawGenus>,
#[serde(default)]
flavor_text_entries: Vec<RawFlavorText>,
}
#[derive(serde::Deserialize)]
struct RawGenus {
genus: String,
language: NamedResource,
}
#[derive(serde::Deserialize)]
struct RawFlavorText {
flavor_text: String,
language: NamedResource,
}
#[derive(serde::Deserialize)]
struct RawChainRef {
url: String,
}
#[derive(serde::Deserialize)]
struct RawEvolutionChain {
chain: RawChainLink,
}
#[derive(serde::Deserialize)]
struct RawChainLink {
species: NamedResource,
evolves_to: Vec<RawChainLink>,
}
#[derive(serde::Deserialize)]
struct MyMemoryResponse {
#[serde(rename = "responseData")]
response_data: MyMemoryData,
}
#[derive(serde::Deserialize)]
struct MyMemoryData {
#[serde(rename = "translatedText")]
translated_text: String,
}