use reqwest::header;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::temp_trait::{CommonCrates, Crate};
const DOCS_BASE_URL: &str = "https://docs.rs/crate";
pub struct OnlineDocs;
impl OnlineDocs {
pub async fn fetch_json<T>(url: &str) -> Result<T>
where
T: CommonCrates + Serialize + for<'de> Deserialize<'de>,
{
let client = reqwest::Client::builder().build()?;
let response = client.get(url).send().await?;
let headers = response.headers().clone();
let body_bytes = response.bytes().await?;
let content_encoding = headers
.get(header::CONTENT_ENCODING)
.and_then(|value| value.to_str().ok());
let content_type = headers
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok());
let decompressed_bytes = Self::decompress_if_needed(
&body_bytes,
content_encoding,
content_type,
url,
)?;
serde_json::from_slice::<T>(&decompressed_bytes).map_err(Error::Json)
}
fn decompress_if_needed(
body_bytes: &[u8],
content_encoding: Option<&str>,
_content_type: Option<&str>,
_url: &str,
) -> Result<Vec<u8>> {
if let Some(encoding) = content_encoding {
if encoding.eq_ignore_ascii_case("zstd") {
return zstd::decode_all(body_bytes).map_err(Error::Io);
} else {
return Ok(body_bytes.to_vec());
}
}
match zstd::decode_all(body_bytes) {
Ok(decompressed) => Ok(decompressed),
Err(_) => {
Ok(body_bytes.to_vec())
}
}
}
pub async fn fetch_docs(
lib_name: &str,
version: Option<String>,
) -> Result<Box<dyn CommonCrates>> {
let version = version.unwrap_or("latest".to_string());
let url = format!("{DOCS_BASE_URL}/{lib_name}/{version}/json");
match OnlineDocs::fetch_json::<rustdoc_types::Crate>(url.as_str()).await {
Ok(result) => Ok(Box::new(result)),
Err(_) => match OnlineDocs::fetch_json::<Crate>(url.as_str()).await {
Ok(result) => Ok(Box::new(result)),
Err(err) => Err(err),
},
}
}
pub async fn fetch_docs_by_url(url: &str) -> Result<Box<dyn CommonCrates>> {
match OnlineDocs::fetch_json::<rustdoc_types::Crate>(url).await {
Ok(result) => Ok(Box::new(result)),
Err(_) => match OnlineDocs::fetch_json::<Crate>(url).await {
Ok(result) => Ok(Box::new(result)),
Err(err) => Err(err),
},
}
}
}
#[cfg(test)]
mod tests {
use crate::{temp_trait::Crate, OnlineDocs, DOCS_BASE_URL};
#[tokio::test]
async fn test_fetch_docs() {
let version = "latest".to_string();
let docs = OnlineDocs::fetch_docs("clap", Some(version.clone()))
.await
.unwrap();
assert!(!docs.crate_version().is_empty());
println!(
"Successfully fetched clap docs, version: {:?}",
docs.crate_version()
);
}
#[tokio::test]
async fn test_fetch_docs_opendal() {
let version = "latest".to_string();
let docs = OnlineDocs::fetch_docs("opendal", Some(version.clone()))
.await
.unwrap();
println!("Successfully fetched docs for opendal");
println!("Crate version: {:?}", docs.crate_version());
assert!(!docs.crate_version().is_empty());
}
#[tokio::test]
async fn test_fetch_docs_json_validation() {
let version = "latest".to_string();
let url = format!("{DOCS_BASE_URL}/serde/{version}/json");
let client = reqwest::Client::builder().build().unwrap();
let response = client.get(&url).send().await.unwrap();
let body_bytes = response.bytes().await.unwrap();
let decompressed_bytes =
OnlineDocs::decompress_if_needed(&body_bytes, None, None, &url).unwrap();
let json_valid =
serde_json::from_slice::<Crate>(&decompressed_bytes).is_ok();
assert!(json_valid, "Downloaded JSON should be structurally valid");
println!("Successfully validated JSON structure for serde docs");
}
#[tokio::test]
async fn test_fetch_docs_json_validation_x() {
let version = "latest".to_string();
let json_valid = OnlineDocs::fetch_docs("serde", Some(version.clone()))
.await
.is_ok();
assert!(json_valid, "Downloaded JSON should be structurally valid");
println!("Successfully validated JSON structure for serde docs");
}
}