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
16pub 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 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}