use std::fmt;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::PdbError;
use crate::core::PdbStructure;
use crate::parser::{parse_mmcif_string, parse_pdb_string};
use super::RCSB_DOWNLOAD_URL;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileFormat {
Pdb,
Cif,
}
impl FileFormat {
pub fn extension(&self) -> &'static str {
match self {
FileFormat::Pdb => "pdb",
FileFormat::Cif => "cif",
}
}
pub fn compressed_extension(&self) -> &'static str {
match self {
FileFormat::Pdb => "pdb.gz",
FileFormat::Cif => "cif.gz",
}
}
}
impl fmt::Display for FileFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileFormat::Pdb => write!(f, "PDB"),
FileFormat::Cif => write!(f, "mmCIF"),
}
}
}
#[derive(Debug)]
pub enum DownloadError {
RequestFailed(String),
NotFound(String),
ParseError(PdbError),
IoError(std::io::Error),
}
impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DownloadError::RequestFailed(msg) => write!(f, "Download failed: {}", msg),
DownloadError::NotFound(pdb_id) => write!(f, "PDB ID not found: {}", pdb_id),
DownloadError::ParseError(err) => write!(f, "Parse error: {}", err),
DownloadError::IoError(err) => write!(f, "I/O error: {}", err),
}
}
}
impl std::error::Error for DownloadError {}
impl From<reqwest::Error> for DownloadError {
fn from(err: reqwest::Error) -> Self {
DownloadError::RequestFailed(err.to_string())
}
}
impl From<PdbError> for DownloadError {
fn from(err: PdbError) -> Self {
DownloadError::ParseError(err)
}
}
impl From<std::io::Error> for DownloadError {
fn from(err: std::io::Error) -> Self {
DownloadError::IoError(err)
}
}
fn build_download_url(pdb_id: &str, format: FileFormat) -> String {
let pdb_id_upper = pdb_id.to_uppercase();
match format {
FileFormat::Pdb => format!("{}/{}.pdb", RCSB_DOWNLOAD_URL, pdb_id_upper),
FileFormat::Cif => format!("{}/{}.cif", RCSB_DOWNLOAD_URL, pdb_id_upper),
}
}
pub fn download_pdb_string(pdb_id: &str, format: FileFormat) -> Result<String, DownloadError> {
let url = build_download_url(pdb_id, format);
let client = reqwest::blocking::Client::new();
let response = client.get(&url).send()?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(DownloadError::NotFound(pdb_id.to_string()));
}
if !response.status().is_success() {
return Err(DownloadError::RequestFailed(format!(
"HTTP {}: {}",
response.status(),
response.text().unwrap_or_default()
)));
}
Ok(response.text()?)
}
pub fn download_structure(pdb_id: &str, format: FileFormat) -> Result<PdbStructure, DownloadError> {
let content = download_pdb_string(pdb_id, format)?;
let structure = match format {
FileFormat::Pdb => parse_pdb_string(&content)?,
FileFormat::Cif => parse_mmcif_string(&content)?,
};
Ok(structure)
}
pub fn download_to_file<P: AsRef<Path>>(
pdb_id: &str,
path: P,
format: FileFormat,
) -> Result<(), DownloadError> {
let content = download_pdb_string(pdb_id, format)?;
let mut file = File::create(path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
pub fn download_multiple(
pdb_ids: &[&str],
format: FileFormat,
) -> Vec<(String, Result<PdbStructure, DownloadError>)> {
pdb_ids
.iter()
.map(|&pdb_id| {
let result = download_structure(pdb_id, format);
(pdb_id.to_string(), result)
})
.collect()
}
pub fn download_multiple_to_files<P: AsRef<Path>>(
pdb_ids: &[&str],
output_dir: P,
format: FileFormat,
) -> Vec<(String, Result<(), DownloadError>)> {
let dir = output_dir.as_ref();
pdb_ids
.iter()
.map(|&pdb_id| {
let filename = format!("{}.{}", pdb_id.to_uppercase(), format.extension());
let path = dir.join(filename);
let result = download_to_file(pdb_id, path, format);
(pdb_id.to_string(), result)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_format_extension() {
assert_eq!(FileFormat::Pdb.extension(), "pdb");
assert_eq!(FileFormat::Cif.extension(), "cif");
}
#[test]
fn test_file_format_compressed_extension() {
assert_eq!(FileFormat::Pdb.compressed_extension(), "pdb.gz");
assert_eq!(FileFormat::Cif.compressed_extension(), "cif.gz");
}
#[test]
fn test_file_format_display() {
assert_eq!(FileFormat::Pdb.to_string(), "PDB");
assert_eq!(FileFormat::Cif.to_string(), "mmCIF");
}
#[test]
fn test_build_download_url_pdb() {
let url = build_download_url("1ubq", FileFormat::Pdb);
assert_eq!(url, "https://files.rcsb.org/download/1UBQ.pdb");
}
#[test]
fn test_build_download_url_cif() {
let url = build_download_url("8hm2", FileFormat::Cif);
assert_eq!(url, "https://files.rcsb.org/download/8HM2.cif");
}
#[test]
fn test_download_error_display() {
let err = DownloadError::NotFound("XXXX".to_string());
assert_eq!(err.to_string(), "PDB ID not found: XXXX");
let err = DownloadError::RequestFailed("timeout".to_string());
assert_eq!(err.to_string(), "Download failed: timeout");
}
}