#[cfg(feature = "rustdoc")]
use std::path::PathBuf;
use fetch_docs::OnlineDocs;
use rustdoc_types::Visibility;
use serde::{Deserialize, Serialize};
use error::{Error, Result};
use temp_trait::CommonCrates;
pub mod error;
pub mod fetch_docs;
#[cfg(feature = "rustdoc")]
mod gen_docs;
pub mod temp_trait;
const DOCS_BASE_URL: &str = "https://docs.rs/crate";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionItem {
pub title: String,
pub description: String,
pub link: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullSessionItem {
pub content: String,
pub link: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateDocs {
pub lib_name: String,
pub version: String,
pub sessions: Vec<SessionItem>,
pub full_sessions: Vec<FullSessionItem>,
}
impl CrateDocs {
pub fn new(lib_name: &str, version: &str) -> Self {
Self {
lib_name: lib_name.to_string(),
version: version.to_string(),
sessions: Vec::new(),
full_sessions: Vec::new(),
}
}
fn process_docs<T: CommonCrates>(
lib_name: &str,
docs: T,
version: Option<String>,
) -> Result<CrateDocs> {
let version = version.unwrap_or_else(|| docs.crate_version());
let mut crate_docs = CrateDocs::new(lib_name, &version);
let base_url =
format!("{}/{}/{}/source", DOCS_BASE_URL, &lib_name, version);
crate_docs.sessions.push(SessionItem {
title: lib_name.to_string(),
description: "".to_string(),
link: format!("https://docs.rs/{lib_name}/{version}"),
});
for (_, item) in docs.index() {
if let Some(docs_content) = item.docs {
if item.visibility != Visibility::Public {
continue;
}
let filename = item.span.unwrap().filename;
let link = format!("{}/{}", base_url, filename.to_str().unwrap());
crate_docs.sessions.push(SessionItem {
title: match item.name {
Some(name) => name,
None => filename.to_str().unwrap().to_string(),
},
description: "".to_string(),
link: link.clone(),
});
crate_docs.full_sessions.push(FullSessionItem {
content: docs_content,
link,
});
};
}
Ok(crate_docs)
}
pub async fn from_online(
lib_name: &str,
version: Option<String>,
) -> Result<CrateDocs> {
let version_str = version.unwrap_or("latest".to_string());
let url = format!("{DOCS_BASE_URL}/{lib_name}/{version_str}/json");
match OnlineDocs::fetch_json::<rustdoc_types::Crate>(url.as_str()).await {
Ok(result) => {
let crate_version = Some(result.crate_version());
CrateDocs::process_docs(lib_name, result, crate_version)
}
Err(_) => {
match OnlineDocs::fetch_json::<temp_trait::Crate>(url.as_str()).await {
Ok(result) => {
let crate_version = Some(result.crate_version());
CrateDocs::process_docs(lib_name, result, crate_version)
}
Err(err) => Err(err),
}
}
}
}
pub async fn from_url(url: &str) -> Result<CrateDocs> {
match OnlineDocs::fetch_json::<rustdoc_types::Crate>(url).await {
Ok(docs) => {
let root_id = docs.root_id();
if let Some(root_item) = docs.index().get(&root_id) {
let lib_name =
&root_item.name.clone().unwrap_or("unknown".to_string());
let crate_version = Some(docs.crate_version());
return CrateDocs::process_docs(lib_name, docs, crate_version);
}
Err(Error::Config(
"Failed to extract crate name from root item".into(),
))
}
Err(_) => {
match OnlineDocs::fetch_json::<temp_trait::Crate>(url).await {
Ok(docs) => {
let root_id = docs.root_id();
if let Some(root_item) = docs.index().get(&root_id) {
let lib_name =
&root_item.name.clone().unwrap_or("unknown".to_string());
let crate_version = Some(docs.crate_version());
return CrateDocs::process_docs(lib_name, docs, crate_version);
}
Err(Error::Config(
"Failed to extract crate name from root item".into(),
))
}
Err(err) => Err(err),
}
}
}
}
#[cfg(feature = "rustdoc")]
pub fn from_local(
manifest_path: PathBuf,
toolchain: Option<String>,
) -> Result<CrateDocs> {
let gen_docs_struct = match toolchain {
Some(toolchain) => {
gen_docs::gen_docs_with_all_features(&toolchain, manifest_path)?
}
None => {
gen_docs::gen_docs_with_all_features_auto_toolchain(manifest_path)?
}
};
let lib_name = gen_docs_struct.lib_name;
let docs = gen_docs_struct.docs;
CrateDocs::process_docs(&lib_name, docs, None)
}
#[cfg(feature = "rustdoc")]
pub fn from_local_with_features(
manifest_path: PathBuf,
no_default_features: bool,
features: Option<Vec<String>>,
toolchain: Option<String>,
) -> Result<CrateDocs> {
let gen_docs_struct = match toolchain {
Some(toolchain) => gen_docs::gen_docs_with_features(
&toolchain,
manifest_path,
no_default_features,
features,
)?,
None => gen_docs::gen_docs_with_features_auto_toolchain(
manifest_path,
no_default_features,
features,
)?,
};
let lib_name = gen_docs_struct.lib_name;
let docs = gen_docs_struct.docs;
CrateDocs::process_docs(&lib_name, docs, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rustdoc")]
use std::path::PathBuf;
#[tokio::test]
async fn test_from_online() {
let lib_name = "clap";
let version = "4.5.42".to_string();
let result = CrateDocs::from_online(lib_name, Some(version.clone())).await;
match result {
Ok(docs) => {
assert_eq!(docs.lib_name, lib_name);
assert_eq!(docs.version, version);
assert!(!docs.sessions.is_empty());
println!("Successfully fetched docs for {}", lib_name);
}
Err(e) => {
match e {
crate::error::Error::Config(msg) if msg.contains("incompatible") => {
println!(
"Expected version compatibility issue with {}: {}",
lib_name, msg
);
}
_ => {
panic!("Unexpected error fetching {} docs: {:?}", lib_name, e);
}
}
}
}
}
#[cfg(feature = "rustdoc")]
#[test]
fn test_from_local_with_all_features() {
let lib_name = "crates_llms_txt";
let current_dir = std::env::current_dir().unwrap();
let docs = CrateDocs::from_local(
current_dir.join("Cargo.toml"),
Some("stable".to_string()),
)
.unwrap();
assert_eq!(docs.lib_name, lib_name);
assert!(!docs.sessions.is_empty());
}
#[cfg(feature = "rustdoc")]
#[test]
fn test_from_local_with_stable_toolchain() {
let current_dir = std::env::current_dir().unwrap();
let manifest_path = current_dir.join("Cargo.toml");
let result =
CrateDocs::from_local(manifest_path.clone(), Some("stable".to_string()));
assert!(result.is_ok());
let docs = result.unwrap();
assert_eq!(docs.lib_name, "crates_llms_txt");
assert!(!docs.sessions.is_empty());
assert!(!docs.full_sessions.is_empty());
}
#[cfg(feature = "rustdoc")]
#[test]
fn test_from_local_with_auto_toolchain() {
let current_dir = std::env::current_dir().unwrap();
let manifest_path = current_dir.join("Cargo.toml");
let result = CrateDocs::from_local(manifest_path.clone(), None);
assert!(result.is_ok());
let docs = result.unwrap();
assert_eq!(docs.lib_name, "crates_llms_txt");
assert!(!docs.sessions.is_empty());
assert!(!docs.full_sessions.is_empty());
}
#[cfg(feature = "rustdoc")]
#[test]
fn test_from_local_invalid_path() {
let invalid_path = PathBuf::from("/invalid/path/Cargo.toml");
let result = CrateDocs::from_local(invalid_path, None);
assert!(result.is_err());
}
#[cfg(feature = "rustdoc")]
#[test]
fn test_from_local_with_features() {
let current_dir = std::env::current_dir().unwrap();
let manifest_path = current_dir.join("Cargo.toml");
let result = CrateDocs::from_local_with_features(
manifest_path.clone(),
false,
Some(vec!["rustdoc".to_string()]),
Some("stable".to_string()),
);
assert!(result.is_ok());
let docs = result.unwrap();
assert_eq!(docs.lib_name, "crates_llms_txt");
assert!(!docs.sessions.is_empty());
assert!(!docs.full_sessions.is_empty());
let result = CrateDocs::from_local_with_features(
manifest_path.clone(),
true,
None,
None,
);
assert!(result.is_ok());
let invalid_path = PathBuf::from("/invalid/path/Cargo.toml");
let result =
CrateDocs::from_local_with_features(invalid_path, false, None, None);
assert!(result.is_err());
}
}