use std::{fs, path::Path};
use serde::Serialize;
use serde_json::{Value, json};
use toko_feed::{
CollectionView, PokemonCardMetadataDetails, PokemonCardPrintingView, PokemonCardView,
PokemonSetView,
};
use super::{
CliError, DEFAULT_MAX_PAGES, ListOptions, MAX_QUERY_LIMIT, Target, call_one,
collect_keyset_pages, fetch_card_page, fetch_card_printing_page, fetch_collection_page,
fetch_set_page,
};
const CANONICAL_BACKUP_FORMAT_VERSION: u32 = 3;
pub const DEFAULT_CANONICAL_BACKUP_PATH: &str = "data/canonical.json";
#[derive(Debug, Serialize)]
struct CanonicalBackup {
format_version: u32,
collections: Vec<CollectionView>,
pokemon_sets: Vec<PokemonSetView>,
pokemon_cards: Vec<PokemonCardView>,
pokemon_card_metadata: Vec<PokemonCardMetadataDetails>,
pokemon_card_printings: Vec<PokemonCardPrintingView>,
}
pub fn execute(target: &Target, path: &Path) -> Result<Value, CliError> {
let options = ListOptions {
limit: MAX_QUERY_LIMIT,
after: None,
all: true,
max_pages: DEFAULT_MAX_PAGES,
};
let (collections, collection_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_collection_page(target, after, limit)?;
Ok((page.collections, page.next_after))
})?;
let (pokemon_sets, set_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_set_page(target, after, limit)?;
Ok((page.sets, page.next_after))
})?;
let (pokemon_cards, card_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_card_page(target, after, limit)?;
Ok((page.cards, page.next_after))
})?;
let (pokemon_card_printings, printing_pages) =
collect_keyset_pages(&options, |after, limit| {
let page = fetch_card_printing_page(target, after, limit)?;
Ok((page.printings, page.next_after))
})?;
let pokemon_card_metadata = pokemon_cards
.iter()
.map(|card| {
call_one::<_, Option<PokemonCardMetadataDetails>>(
target,
"toko_feed_card_metadata",
card.id.clone(),
true,
)
})
.collect::<Result<Vec<_>, CliError>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let backup = CanonicalBackup {
format_version: CANONICAL_BACKUP_FORMAT_VERSION,
collections,
pokemon_sets,
pokemon_cards,
pokemon_card_metadata,
pokemon_card_printings,
};
write_backup(path, &backup)?;
Ok(json!({
"path": path,
"format_version": CANONICAL_BACKUP_FORMAT_VERSION,
"collections": backup.collections.len(),
"pokemon_sets": backup.pokemon_sets.len(),
"pokemon_cards": backup.pokemon_cards.len(),
"pokemon_card_metadata": backup.pokemon_card_metadata.len(),
"pokemon_card_printings": backup.pokemon_card_printings.len(),
"pages": collection_pages
.saturating_add(set_pages)
.saturating_add(card_pages)
.saturating_add(printing_pages),
}))
}
fn write_backup(path: &Path, backup: &CanonicalBackup) -> Result<(), CliError> {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty());
if let Some(parent) = parent {
fs::create_dir_all(parent).map_err(|source| CliError::BackupIo {
action: "create parent for",
path: path.to_owned(),
source,
})?;
}
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
CliError::Usage("canonical backup path must name a UTF-8 JSON file".to_owned())
})?;
let temporary = path.with_file_name(format!(".{file_name}.{}.tmp", std::process::id()));
let mut bytes = serde_json::to_vec_pretty(backup)?;
bytes.push(b'\n');
fs::write(&temporary, bytes).map_err(|source| CliError::BackupIo {
action: "write",
path: temporary.clone(),
source,
})?;
if let Err(source) = fs::rename(&temporary, path) {
let _ = fs::remove_file(&temporary);
return Err(CliError::BackupIo {
action: "replace",
path: path.to_owned(),
source,
});
}
Ok(())
}