Skip to main content

kellnr_docs/
lib.rs

1pub mod api;
2mod doc_archive;
3mod doc_queue_response;
4pub mod docs_error;
5pub mod unpack;
6pub mod upload;
7mod upload_response;
8
9use kellnr_common::original_name::OriginalName;
10use kellnr_common::version::Version;
11use kellnr_settings::UrlPathPrefix;
12use kellnr_storage::docs_storage::{DocsName, DocsStorage};
13
14use crate::docs_error::DocsError;
15
16/// URL of the docs of a crate version. The docs are stored under the
17/// lowercase crate name, but rustdoc keeps the casing of the crate name for
18/// its output folder.
19pub fn compute_doc_url(
20    crate_name: &OriginalName,
21    crate_version: &Version,
22    path_prefix: &UrlPathPrefix,
23) -> String {
24    let folder = DocsName::from(crate_name);
25    let docs_name = crate_name_to_docs_name(crate_name);
26    format!("{path_prefix}/docs/{folder}/{crate_version}/doc/{docs_name}/index.html")
27}
28
29pub fn crate_name_to_docs_name(crate_name: &OriginalName) -> String {
30    // Cargo replaces the `-` with `_` in the crate name when
31    // docs are generated. As such, the docs folder name is not "foo-bar" but "foo_bar".
32    crate_name.replace('-', "_")
33}
34
35async fn doc_exists(
36    crate_name: &OriginalName,
37    crate_version: &Version,
38    docs_storage: &DocsStorage,
39) -> bool {
40    let docs_name = crate_name_to_docs_name(crate_name);
41    let key = DocsStorage::file_key(
42        &DocsName::from(crate_name),
43        crate_version,
44        &format!("doc/{docs_name}/index.html"),
45    );
46    docs_storage.exists(&key).await.unwrap_or(false)
47}
48
49pub async fn delete(
50    crate_name: &OriginalName,
51    crate_version: &Version,
52    docs_storage: &DocsStorage,
53) -> Result<(), DocsError> {
54    docs_storage
55        .delete_prefix(&DocsStorage::version_prefix(
56            &DocsName::from(crate_name),
57            crate_version,
58        ))
59        .await?;
60    Ok(())
61}
62
63#[cfg(test)]
64mod tests {
65    use bytes::Bytes;
66    use kellnr_storage::fs_storage::FSStorage;
67    use kellnr_storage::object_storage::MemoryStorage;
68
69    use super::*;
70
71    #[test]
72    fn compute_doc_url_without_path_prefix() {
73        let version = Version::try_from("1.0.0").unwrap();
74        let url = compute_doc_url(&original("my-crate"), &version, &UrlPathPrefix::default());
75        assert_eq!(url, "/docs/my-crate/1.0.0/doc/my_crate/index.html");
76    }
77
78    #[test]
79    fn compute_doc_url_with_path_prefix() {
80        let version = Version::try_from("1.0.0").unwrap();
81        let url = compute_doc_url(
82            &original("my-crate"),
83            &version,
84            &UrlPathPrefix::new("/kellnr"),
85        );
86        assert_eq!(url, "/kellnr/docs/my-crate/1.0.0/doc/my_crate/index.html");
87    }
88
89    #[test]
90    fn compute_doc_url_replaces_hyphen_with_underscore_in_docs_name() {
91        let version = Version::try_from("2.0.0-beta1").unwrap();
92        let url = compute_doc_url(
93            &original("foo-bar-baz"),
94            &version,
95            &UrlPathPrefix::default(),
96        );
97        assert_eq!(
98            url,
99            "/docs/foo-bar-baz/2.0.0-beta1/doc/foo_bar_baz/index.html"
100        );
101    }
102
103    #[test]
104    fn compute_doc_url_keeps_casing_of_docs_name_only() {
105        let version = Version::try_from("1.0.0").unwrap();
106        let url = compute_doc_url(&original("My-Crate"), &version, &UrlPathPrefix::default());
107        assert_eq!(url, "/docs/my-crate/1.0.0/doc/My_Crate/index.html");
108    }
109
110    fn original(name: &str) -> OriginalName {
111        OriginalName::try_from(name).unwrap()
112    }
113
114    fn docs_storage() -> (tempfile::TempDir, DocsStorage) {
115        let dir = tempfile::tempdir().unwrap();
116        let storage = FSStorage::new(dir.path().to_str().unwrap()).unwrap();
117        (dir, DocsStorage::new(Box::new(storage)))
118    }
119
120    #[tokio::test]
121    async fn doc_exists_true_when_index_html_present() {
122        let (_dir, docs) = docs_storage();
123        let key = DocsStorage::file_key(
124            &DocsName::from(&original("my-crate")),
125            &Version::try_from("1.0.0").unwrap(),
126            "doc/my_crate/index.html",
127        );
128        docs.put(&key, Bytes::from_static(b"x")).await.unwrap();
129
130        assert!(
131            doc_exists(
132                &original("my-crate"),
133                &Version::try_from("1.0.0").unwrap(),
134                &docs
135            )
136            .await
137        );
138    }
139
140    #[tokio::test]
141    async fn doc_exists_false_when_missing() {
142        let (_dir, docs) = docs_storage();
143        assert!(
144            !doc_exists(
145                &original("my-crate"),
146                &Version::try_from("1.0.0").unwrap(),
147                &docs
148            )
149            .await
150        );
151    }
152
153    #[tokio::test]
154    async fn doc_exists_for_crate_with_uppercase_letters() {
155        let docs = DocsStorage::new(Box::new(MemoryStorage::new()));
156        let key = DocsStorage::file_key(
157            &DocsName::from(&original("my-crate")),
158            &Version::try_from("1.0.0").unwrap(),
159            "doc/My_Crate/index.html",
160        );
161        docs.put(&key, Bytes::from_static(b"x")).await.unwrap();
162
163        assert!(
164            doc_exists(
165                &original("My-Crate"),
166                &Version::try_from("1.0.0").unwrap(),
167                &docs
168            )
169            .await
170        );
171    }
172
173    #[tokio::test]
174    async fn delete_removes_only_targeted_version() {
175        let (_dir, docs) = docs_storage();
176        let v1 = DocsStorage::file_key(
177            &DocsName::from(&original("my-crate")),
178            &Version::try_from("1.0.0").unwrap(),
179            "doc/my_crate/index.html",
180        );
181        let v2 = DocsStorage::file_key(
182            &DocsName::from(&original("my-crate")),
183            &Version::try_from("2.0.0").unwrap(),
184            "doc/my_crate/index.html",
185        );
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(
190            &original("my-crate"),
191            &Version::try_from("1.0.0").unwrap(),
192            &docs,
193        )
194        .await
195        .unwrap();
196
197        assert!(!docs.exists(&v1).await.unwrap());
198        assert!(docs.exists(&v2).await.unwrap());
199    }
200}