This commit is contained in:
2025-07-20 13:38:44 +02:00
commit a58df1cb8e
35 changed files with 4108 additions and 0 deletions

127
src/bin/fetch_characters.rs Normal file
View File

@@ -0,0 +1,127 @@
use mongodb::{
Client,
bson::{doc, to_document},
options::UpdateOptions,
};
use reqwest::Client as HttpClient;
use rick_and_morty::models::{Character, OriginOrLocation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct ApiCharacter {
id: i32,
name: String,
status: String,
species: String,
#[serde(rename = "type")]
character_type: String,
gender: String,
origin: OriginOrLocation,
location: OriginOrLocation,
image: String,
episode: Vec<String>,
url: String,
created: String,
}
impl From<ApiCharacter> for Character {
fn from(api: ApiCharacter) -> Self {
Character {
id: None, // always None for new/incoming data
rmid: api.id,
name: api.name,
status: api.status,
species: api.species,
r#type: api.character_type,
gender: api.gender,
origin: api.origin,
location: api.location,
image: api.image,
episode: api.episode,
url: api.url,
created: api.created,
elo_rating: 1000.0,
}
}
}
fn init_tracing() {
use tracing_subscriber::EnvFilter;
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(true)
.with_level(true)
.init();
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
init_tracing();
dotenvy::dotenv().ok();
let db_uri =
std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string());
let db_name = std::env::var("DB_NAME").unwrap_or_else(|_| "rick_and_morty".to_string());
let client = Client::with_uri_str(&db_uri).await?;
let db = client.database(&db_name);
let collection = db.collection::<Character>("characters");
tracing::info!("Starting to fetch characters from Rick and Morty API");
let http = HttpClient::new();
let mut all_characters: Vec<Character> = Vec::new();
let mut next_url = "https://rickandmortyapi.com/api/character".to_string();
while !next_url.is_empty() {
tracing::info!(url = %next_url, "Fetching page");
let resp = http
.get(&next_url)
.send()
.await?
.json::<serde_json::Value>()
.await?;
let results = resp["results"].as_array().unwrap();
for c in results {
let c: ApiCharacter = serde_json::from_value(c.clone()).unwrap();
all_characters.push(c.into());
}
next_url = resp["info"]["next"].as_str().unwrap_or("").to_string();
}
tracing::info!(
count = all_characters.len(),
"Fetched all characters, starting DB upsert"
);
let options = UpdateOptions::builder().upsert(true).build();
// let insert_result = collection.insert_many(all_characters.clone()).await?;
for character in &all_characters {
let filter = doc! { "rmid": character.rmid };
let mut set_doc = to_document(character)?;
set_doc.remove("elo_rating"); // Do NOT overwrite existing Elo
let update = doc! {
"$set": set_doc,
"$setOnInsert": { "elo_rating": 1000.0 }
};
if let Err(e) = collection
.update_one(filter, update)
.with_options(Some(options.clone()))
.await
{
tracing::error!(error = ?e, id = character.rmid, name = %character.name, "Failed to upsert character");
}
tracing::info!(id = character.rmid, name = %character.name, "Upserted character");
}
// tracing::info!("Inserted {} characters", insert_result.inserted_ids.len());
let character_count = collection.count_documents(doc! {}).await?;
tracing::info!(
count = character_count,
"Total characters in DB after import"
);
tracing::info!("Done! Imported/updated characters.");
Ok(())
}