Skip to main content

kellnr_docs/
lib.rs

1pub mod api;
2pub mod build_process;
3mod doc_archive;
4pub mod doc_queue;
5pub mod doc_queue_response;
6pub mod docs_error;
7mod unpack;
8pub mod upload;
9pub mod upload_response;
10
11use std::convert::TryFrom;
12
13use kellnr_common::version::Version;
14use kellnr_storage::docs_storage::DocsStorage;
15
16use crate::docs_error::DocsError;
17
18pub async fn get_latest_doc_url(
19    crate_name: &str,
20    docs_storage: &DocsStorage,
21    path_prefix: &str,
22) -> Option<String> {
23    let version = get_latest_version_with_doc(crate_name, docs_storage).await?;
24    Some(compute_doc_url(crate_name, &version, path_prefix))
25}
26
27pub async fn get_doc_url(
28    crate_name: &str,
29    crate_version: &Version,
30    docs_storage: &DocsStorage,
31    path_prefix: &str,
32) -> Option<String> {
33    if doc_exists(crate_name, &crate_version.to_string(), docs_storage).await {
34        Some(compute_doc_url(crate_name, crate_version, path_prefix))
35    } else {
36        None
37    }
38}
39
40pub fn compute_doc_url(crate_name: &str, crate_version: &Version, path_prefix: &str) -> String {
41    let docs_name = crate_name_to_docs_name(crate_name);
42    let path_prefix = path_prefix.trim();
43    format!("{path_prefix}/docs/{crate_name}/{crate_version}/doc/{docs_name}/index.html")
44}
45
46fn crate_name_to_docs_name(crate_name: &str) -> String {
47    // Cargo replaces the `-` with `_` in the crate name when
48    // docs are generated. As such, the docs folder name is not "foo-bar" but "foo_bar".
49    crate_name.replace('-', "_")
50}
51
52async fn doc_exists(crate_name: &str, crate_version: &str, docs_storage: &DocsStorage) -> bool {
53    let docs_name = crate_name_to_docs_name(crate_name);
54    let key = DocsStorage::file_key(
55        crate_name,
56        crate_version,
57        &format!("doc/{docs_name}/index.html"),
58    );
59    docs_storage.exists(&key).await.unwrap_or(false)
60}
61
62async fn get_latest_version_with_doc(
63    crate_name: &str,
64    docs_storage: &DocsStorage,
65) -> Option<Version> {
66    let mut versions: Vec<Version> = docs_storage
67        .version_candidates(crate_name)
68        .await
69        .ok()?
70        .into_iter()
71        .flat_map(|v| Version::try_from(&v))
72        .collect();
73
74    // Sort and reverse the order such that the biggest version
75    // for which docs exist will be returned.
76    versions.sort();
77    versions.reverse();
78
79    for version in versions {
80        if doc_exists(crate_name, &version.to_string(), docs_storage).await {
81            return Some(version);
82        }
83    }
84    None
85}
86
87pub async fn delete(
88    crate_name: &str,
89    crate_version: &str,
90    docs_storage: &DocsStorage,
91) -> Result<(), DocsError> {
92    docs_storage
93        .delete_prefix(&DocsStorage::version_prefix(crate_name, crate_version))
94        .await?;
95    Ok(())
96}
97
98#[cfg(test)]
99mod tests {
100    use bytes::Bytes;
101    use kellnr_storage::fs_storage::FSStorage;
102
103    use super::*;
104
105    #[test]
106    fn compute_doc_url_without_path_prefix() {
107        let version = Version::try_from("1.0.0").unwrap();
108        let url = compute_doc_url("my-crate", &version, "");
109        assert_eq!(url, "/docs/my-crate/1.0.0/doc/my_crate/index.html");
110    }
111
112    #[test]
113    fn compute_doc_url_with_path_prefix() {
114        let version = Version::try_from("1.0.0").unwrap();
115        let url = compute_doc_url("my-crate", &version, "/kellnr");
116        assert_eq!(url, "/kellnr/docs/my-crate/1.0.0/doc/my_crate/index.html");
117    }
118
119    #[test]
120    fn compute_doc_url_trims_whitespace_from_path_prefix() {
121        let version = Version::try_from("1.0.0").unwrap();
122        let url = compute_doc_url("my-crate", &version, "  /kellnr  ");
123        assert_eq!(url, "/kellnr/docs/my-crate/1.0.0/doc/my_crate/index.html");
124    }
125
126    #[test]
127    fn compute_doc_url_replaces_hyphen_with_underscore_in_docs_name() {
128        let version = Version::try_from("2.0.0-beta1").unwrap();
129        let url = compute_doc_url("foo-bar-baz", &version, "");
130        assert_eq!(
131            url,
132            "/docs/foo-bar-baz/2.0.0-beta1/doc/foo_bar_baz/index.html"
133        );
134    }
135
136    fn docs_storage() -> (tempfile::TempDir, DocsStorage) {
137        let dir = tempfile::tempdir().unwrap();
138        let storage = FSStorage::new(dir.path().to_str().unwrap()).unwrap();
139        (dir, DocsStorage::new(Box::new(storage)))
140    }
141
142    #[tokio::test]
143    async fn doc_exists_true_when_index_html_present() {
144        let (_dir, docs) = docs_storage();
145        let key = DocsStorage::file_key("my-crate", "1.0.0", "doc/my_crate/index.html");
146        docs.put(&key, Bytes::from_static(b"x")).await.unwrap();
147
148        assert!(doc_exists("my-crate", "1.0.0", &docs).await);
149    }
150
151    #[tokio::test]
152    async fn doc_exists_false_when_missing() {
153        let (_dir, docs) = docs_storage();
154        assert!(!doc_exists("my-crate", "1.0.0", &docs).await);
155    }
156
157    #[tokio::test]
158    async fn get_latest_version_with_doc_returns_highest_version() {
159        let (_dir, docs) = docs_storage();
160        for v in ["1.0.0", "2.0.0", "1.5.0"] {
161            let key = DocsStorage::file_key("my-crate", v, "doc/my_crate/index.html");
162            docs.put(&key, Bytes::from_static(b"x")).await.unwrap();
163        }
164
165        let latest = get_latest_version_with_doc("my-crate", &docs)
166            .await
167            .unwrap();
168        assert_eq!(latest.to_string(), "2.0.0");
169    }
170
171    #[tokio::test]
172    async fn get_latest_version_with_doc_none_when_no_versions() {
173        let (_dir, docs) = docs_storage();
174        assert!(
175            get_latest_version_with_doc("my-crate", &docs)
176                .await
177                .is_none()
178        );
179    }
180
181    #[tokio::test]
182    async fn delete_removes_only_targeted_version() {
183        let (_dir, docs) = docs_storage();
184        let v1 = DocsStorage::file_key("my-crate", "1.0.0", "doc/my_crate/index.html");
185        let v2 = DocsStorage::file_key("my-crate", "2.0.0", "doc/my_crate/index.html");
186        docs.put(&v1, Bytes::from_static(b"a")).await.unwrap();
187        docs.put(&v2, Bytes::from_static(b"b")).await.unwrap();
188
189        delete("my-crate", "1.0.0", &docs).await.unwrap();
190
191        assert!(!docs.exists(&v1).await.unwrap());
192        assert!(docs.exists(&v2).await.unwrap());
193    }
194}