1use provcfg::{ClapArgs, Configurable};
2use serde::{Deserialize, Serialize};
3use url::Url;
4
5fn default_proxy_url() -> Url {
6 Url::parse("https://static.crates.io/crates/").unwrap()
7}
8
9fn default_index_url() -> Url {
10 Url::parse("https://index.crates.io/").unwrap()
11}
12
13fn default_api_url() -> Url {
14 Url::parse("https://crates.io/api/v1/crates/").unwrap()
15}
16
17fn default_user_agent() -> String {
18 kellnr_common::cratesio_downloader::DEFAULT_USER_AGENT.to_string()
19}
20
21#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
22#[serde(default)]
23#[configurable(clap_prefix = "proxy")]
24pub struct Proxy {
25 pub enabled: bool,
27
28 pub num_threads: usize,
30
31 pub download_on_update: bool,
33
34 pub url: Url,
36
37 pub index: Url,
39
40 pub api: Url,
42
43 #[arg(long = "proxy-connect-timeout")]
45 pub connect_timeout_seconds: u64,
46
47 #[arg(long = "proxy-request-timeout")]
49 pub request_timeout_seconds: u64,
50
51 #[arg(long = "proxy-user-agent")]
55 pub user_agent: String,
56}
57
58impl Default for Proxy {
59 fn default() -> Self {
60 Self {
61 enabled: false,
62 num_threads: 10,
63 download_on_update: false,
64 url: default_proxy_url(),
65 index: default_index_url(),
66 api: default_api_url(),
67 connect_timeout_seconds: 5,
68 request_timeout_seconds: 30,
69 user_agent: default_user_agent(),
70 }
71 }
72}
73
74impl Proxy {
75 pub fn cratesio_index_override(&self) -> Option<&Url> {
85 if self.enabled && self.index != default_index_url() {
86 Some(&self.index)
87 } else {
88 None
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn default_api_url_is_cratesio() {
99 let proxy = Proxy::default();
100 assert_eq!(
101 proxy.api,
102 Url::parse("https://crates.io/api/v1/crates/").unwrap()
103 );
104 }
105
106 #[test]
107 fn deserialize_custom_api_url_from_toml() {
108 let toml = r#"
109 api = "https://rsproxy.cn/api/v1/crates/"
110 "#;
111 let proxy: Proxy = toml::from_str(toml).unwrap();
112 assert_eq!(
113 proxy.api,
114 Url::parse("https://rsproxy.cn/api/v1/crates/").unwrap()
115 );
116 }
117
118 #[test]
119 fn missing_api_url_uses_default() {
120 let toml = r#"
121 enabled = true
122 url = "https://rsproxy.cn/api/v1/crates/"
123 index = "https://rsproxy.cn/index/"
124 "#;
125 let proxy: Proxy = toml::from_str(toml).unwrap();
126 assert_eq!(
127 proxy.api,
128 Url::parse("https://crates.io/api/v1/crates/").unwrap()
129 );
130 }
131
132 #[test]
133 fn api_url_join_produces_correct_crate_url() {
134 let proxy: Proxy = toml::from_str(r#"api = "https://rsproxy.cn/api/v1/crates/""#).unwrap();
135 let url = proxy.api.join("serde").unwrap();
136 assert_eq!(url.as_str(), "https://rsproxy.cn/api/v1/crates/serde");
137 }
138
139 #[test]
140 fn default_url_join_produces_correct_crate_url() {
141 let proxy = Proxy::default();
142 let url = proxy.api.join("tokio").unwrap();
143 assert_eq!(url.as_str(), "https://crates.io/api/v1/crates/tokio");
144 }
145
146 #[test]
147 fn default_user_agent_is_kellnr() {
148 let proxy = Proxy::default();
149 assert_eq!(proxy.user_agent, "kellnr.io/kellnr");
150 }
151
152 #[test]
153 fn missing_user_agent_uses_default() {
154 let proxy: Proxy = toml::from_str("enabled = true").unwrap();
155 assert_eq!(proxy.user_agent, "kellnr.io/kellnr");
156 }
157
158 #[test]
159 fn deserialize_custom_user_agent_from_toml() {
160 let toml = r#"user_agent = "kellnr.io/kellnr (admin@example.com)""#;
161 let proxy: Proxy = toml::from_str(toml).unwrap();
162 assert_eq!(proxy.user_agent, "kellnr.io/kellnr (admin@example.com)");
163 }
164
165 #[test]
166 fn index_override_none_when_disabled() {
167 let proxy: Proxy = toml::from_str(r#"index = "https://rsproxy.cn/index/""#).unwrap();
168 assert!(!proxy.enabled);
169 assert_eq!(proxy.cratesio_index_override(), None);
170 }
171
172 #[test]
173 fn index_override_none_when_default_index() {
174 let proxy: Proxy = toml::from_str("enabled = true").unwrap();
175 assert_eq!(proxy.cratesio_index_override(), None);
176 }
177
178 #[test]
179 fn index_override_some_when_enabled_and_custom() {
180 let toml = r#"
181 enabled = true
182 index = "https://rsproxy.cn/index/"
183 "#;
184 let proxy: Proxy = toml::from_str(toml).unwrap();
185 assert_eq!(
186 proxy.cratesio_index_override().map(Url::as_str),
187 Some("https://rsproxy.cn/index/")
188 );
189 }
190}