1use std::path::Path;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use url::Url;
7
8use uv_auth::{AuthPolicy, Credentials};
9use uv_redacted::DisplaySafeUrl;
10use uv_small_str::SmallString;
11
12use crate::index_name::{IndexName, IndexNameError};
13use crate::origin::Origin;
14use crate::{IndexStatusCodeStrategy, IndexUrl, IndexUrlError, SerializableStatusCode};
15
16#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Default)]
18#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
19#[serde(rename_all = "kebab-case")]
20pub struct IndexCacheControl {
21 pub api: Option<SmallString>,
23 pub files: Option<SmallString>,
25}
26
27impl IndexCacheControl {
28 pub fn simple_api_cache_control(_url: &Url) -> Option<&'static str> {
30 None
31 }
32
33 pub fn artifact_cache_control(url: &Url) -> Option<&'static str> {
35 if url
36 .host_str()
37 .is_some_and(|host| host.ends_with("pytorch.org"))
38 {
39 Some("max-age=365000000, immutable, public")
45 } else {
46 None
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53#[serde(rename_all = "kebab-case")]
54pub struct Index {
55 pub name: Option<IndexName>,
69 pub url: IndexUrl,
73 #[serde(default)]
88 pub explicit: bool,
89 #[serde(default)]
99 pub default: bool,
100 #[serde(skip)]
102 pub origin: Option<Origin>,
103 #[serde(default)]
109 pub format: IndexFormat,
110 pub publish_url: Option<DisplaySafeUrl>,
123 #[serde(default)]
132 pub authenticate: AuthPolicy,
133 #[serde(default)]
143 pub ignore_error_codes: Option<Vec<SerializableStatusCode>>,
144 #[serde(default)]
156 pub cache_control: Option<IndexCacheControl>,
157}
158
159impl PartialEq for Index {
160 fn eq(&self, other: &Self) -> bool {
161 let Self {
162 name,
163 url,
164 explicit,
165 default,
166 origin: _,
167 format,
168 publish_url,
169 authenticate,
170 ignore_error_codes,
171 cache_control,
172 } = self;
173 *url == other.url
174 && *name == other.name
175 && *explicit == other.explicit
176 && *default == other.default
177 && *format == other.format
178 && *publish_url == other.publish_url
179 && *authenticate == other.authenticate
180 && *ignore_error_codes == other.ignore_error_codes
181 && *cache_control == other.cache_control
182 }
183}
184
185impl Eq for Index {}
186
187impl PartialOrd for Index {
188 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
189 Some(self.cmp(other))
190 }
191}
192
193impl Ord for Index {
194 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
195 let Self {
196 name,
197 url,
198 explicit,
199 default,
200 origin: _,
201 format,
202 publish_url,
203 authenticate,
204 ignore_error_codes,
205 cache_control,
206 } = self;
207 url.cmp(&other.url)
208 .then_with(|| name.cmp(&other.name))
209 .then_with(|| explicit.cmp(&other.explicit))
210 .then_with(|| default.cmp(&other.default))
211 .then_with(|| format.cmp(&other.format))
212 .then_with(|| publish_url.cmp(&other.publish_url))
213 .then_with(|| authenticate.cmp(&other.authenticate))
214 .then_with(|| ignore_error_codes.cmp(&other.ignore_error_codes))
215 .then_with(|| cache_control.cmp(&other.cache_control))
216 }
217}
218
219impl std::hash::Hash for Index {
220 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
221 let Self {
222 name,
223 url,
224 explicit,
225 default,
226 origin: _,
227 format,
228 publish_url,
229 authenticate,
230 ignore_error_codes,
231 cache_control,
232 } = self;
233 url.hash(state);
234 name.hash(state);
235 explicit.hash(state);
236 default.hash(state);
237 format.hash(state);
238 publish_url.hash(state);
239 authenticate.hash(state);
240 ignore_error_codes.hash(state);
241 cache_control.hash(state);
242 }
243}
244
245#[derive(
246 Default,
247 Debug,
248 Copy,
249 Clone,
250 Hash,
251 Eq,
252 PartialEq,
253 Ord,
254 PartialOrd,
255 serde::Serialize,
256 serde::Deserialize,
257)]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259#[serde(rename_all = "kebab-case")]
260pub enum IndexFormat {
261 #[default]
263 Simple,
264 Flat,
266}
267
268impl Index {
269 pub fn from_index_url(url: IndexUrl) -> Self {
271 Self {
272 url,
273 name: None,
274 explicit: false,
275 default: true,
276 origin: None,
277 format: IndexFormat::Simple,
278 publish_url: None,
279 authenticate: AuthPolicy::default(),
280 ignore_error_codes: None,
281 cache_control: None,
282 }
283 }
284
285 pub fn from_extra_index_url(url: IndexUrl) -> Self {
287 Self {
288 url,
289 name: None,
290 explicit: false,
291 default: false,
292 origin: None,
293 format: IndexFormat::Simple,
294 publish_url: None,
295 authenticate: AuthPolicy::default(),
296 ignore_error_codes: None,
297 cache_control: None,
298 }
299 }
300
301 pub fn from_find_links(url: IndexUrl) -> Self {
303 Self {
304 url,
305 name: None,
306 explicit: false,
307 default: false,
308 origin: None,
309 format: IndexFormat::Flat,
310 publish_url: None,
311 authenticate: AuthPolicy::default(),
312 ignore_error_codes: None,
313 cache_control: None,
314 }
315 }
316
317 #[must_use]
319 pub fn with_origin(mut self, origin: Origin) -> Self {
320 self.origin = Some(origin);
321 self
322 }
323
324 pub fn url(&self) -> &IndexUrl {
326 &self.url
327 }
328
329 pub fn into_url(self) -> IndexUrl {
331 self.url
332 }
333
334 pub fn raw_url(&self) -> &DisplaySafeUrl {
336 self.url.url()
337 }
338
339 pub fn root_url(&self) -> Option<DisplaySafeUrl> {
344 self.url.root()
345 }
346
347 pub fn credentials(&self) -> Option<Credentials> {
349 if let Some(name) = self.name.as_ref() {
351 if let Some(credentials) = Credentials::from_env(name.to_env_var()) {
352 return Some(credentials);
353 }
354 }
355
356 Credentials::from_url(self.url.url())
358 }
359
360 pub fn relative_to(mut self, root_dir: &Path) -> Result<Self, IndexUrlError> {
362 if let IndexUrl::Path(ref url) = self.url {
363 if let Some(given) = url.given() {
364 self.url = IndexUrl::parse(given, Some(root_dir))?;
365 }
366 }
367 Ok(self)
368 }
369
370 pub fn status_code_strategy(&self) -> IndexStatusCodeStrategy {
372 if let Some(ignore_error_codes) = &self.ignore_error_codes {
373 IndexStatusCodeStrategy::from_ignored_error_codes(ignore_error_codes)
374 } else {
375 IndexStatusCodeStrategy::from_index_url(self.url.url())
376 }
377 }
378
379 pub fn artifact_cache_control(&self) -> Option<&str> {
381 if let Some(artifact_cache_control) = self
382 .cache_control
383 .as_ref()
384 .and_then(|cache_control| cache_control.files.as_deref())
385 {
386 Some(artifact_cache_control)
387 } else {
388 IndexCacheControl::artifact_cache_control(self.url.url())
389 }
390 }
391
392 pub fn simple_api_cache_control(&self) -> Option<&str> {
394 if let Some(api_cache_control) = self
395 .cache_control
396 .as_ref()
397 .and_then(|cache_control| cache_control.api.as_deref())
398 {
399 Some(api_cache_control)
400 } else {
401 IndexCacheControl::simple_api_cache_control(self.url.url())
402 }
403 }
404}
405
406impl From<IndexUrl> for Index {
407 fn from(value: IndexUrl) -> Self {
408 Self {
409 name: None,
410 url: value,
411 explicit: false,
412 default: false,
413 origin: None,
414 format: IndexFormat::Simple,
415 publish_url: None,
416 authenticate: AuthPolicy::default(),
417 ignore_error_codes: None,
418 cache_control: None,
419 }
420 }
421}
422
423impl FromStr for Index {
424 type Err = IndexSourceError;
425
426 fn from_str(s: &str) -> Result<Self, Self::Err> {
427 if let Some((name, url)) = s.split_once('=') {
429 if !name.chars().any(|c| c == ':') {
430 let name = IndexName::from_str(name)?;
431 let url = IndexUrl::from_str(url)?;
432 return Ok(Self {
433 name: Some(name),
434 url,
435 explicit: false,
436 default: false,
437 origin: None,
438 format: IndexFormat::Simple,
439 publish_url: None,
440 authenticate: AuthPolicy::default(),
441 ignore_error_codes: None,
442 cache_control: None,
443 });
444 }
445 }
446
447 let url = IndexUrl::from_str(s)?;
449 Ok(Self {
450 name: None,
451 url,
452 explicit: false,
453 default: false,
454 origin: None,
455 format: IndexFormat::Simple,
456 publish_url: None,
457 authenticate: AuthPolicy::default(),
458 ignore_error_codes: None,
459 cache_control: None,
460 })
461 }
462}
463
464#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
466pub struct IndexMetadata {
467 pub url: IndexUrl,
469 pub format: IndexFormat,
471}
472
473impl IndexMetadata {
474 pub fn as_ref(&self) -> IndexMetadataRef<'_> {
476 let Self { url, format: kind } = self;
477 IndexMetadataRef { url, format: *kind }
478 }
479
480 pub fn into_url(self) -> IndexUrl {
482 self.url
483 }
484}
485
486#[derive(Debug, Copy, Clone)]
488pub struct IndexMetadataRef<'a> {
489 pub url: &'a IndexUrl,
491 pub format: IndexFormat,
493}
494
495impl IndexMetadata {
496 pub fn url(&self) -> &IndexUrl {
498 &self.url
499 }
500}
501
502impl IndexMetadataRef<'_> {
503 pub fn url(&self) -> &IndexUrl {
505 self.url
506 }
507}
508
509impl<'a> From<&'a Index> for IndexMetadataRef<'a> {
510 fn from(value: &'a Index) -> Self {
511 Self {
512 url: &value.url,
513 format: value.format,
514 }
515 }
516}
517
518impl<'a> From<&'a IndexMetadata> for IndexMetadataRef<'a> {
519 fn from(value: &'a IndexMetadata) -> Self {
520 Self {
521 url: &value.url,
522 format: value.format,
523 }
524 }
525}
526
527impl From<IndexUrl> for IndexMetadata {
528 fn from(value: IndexUrl) -> Self {
529 Self {
530 url: value,
531 format: IndexFormat::Simple,
532 }
533 }
534}
535
536impl<'a> From<&'a IndexUrl> for IndexMetadataRef<'a> {
537 fn from(value: &'a IndexUrl) -> Self {
538 Self {
539 url: value,
540 format: IndexFormat::Simple,
541 }
542 }
543}
544
545#[derive(Error, Debug)]
547pub enum IndexSourceError {
548 #[error(transparent)]
549 Url(#[from] IndexUrlError),
550 #[error(transparent)]
551 IndexName(#[from] IndexNameError),
552 #[error("Index included a name, but the name was empty")]
553 EmptyName,
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn test_index_cache_control_headers() {
562 let toml_str = r#"
564 name = "test-index"
565 url = "https://test.example.com/simple"
566 cache-control = { api = "max-age=600", files = "max-age=3600" }
567 "#;
568
569 let index: Index = toml::from_str(toml_str).unwrap();
570 assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
571 assert!(index.cache_control.is_some());
572 let cache_control = index.cache_control.as_ref().unwrap();
573 assert_eq!(cache_control.api.as_deref(), Some("max-age=600"));
574 assert_eq!(cache_control.files.as_deref(), Some("max-age=3600"));
575 }
576
577 #[test]
578 fn test_index_without_cache_control() {
579 let toml_str = r#"
581 name = "test-index"
582 url = "https://test.example.com/simple"
583 "#;
584
585 let index: Index = toml::from_str(toml_str).unwrap();
586 assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
587 assert_eq!(index.cache_control, None);
588 }
589
590 #[test]
591 fn test_index_partial_cache_control() {
592 let toml_str = r#"
594 name = "test-index"
595 url = "https://test.example.com/simple"
596 cache-control = { api = "max-age=300" }
597 "#;
598
599 let index: Index = toml::from_str(toml_str).unwrap();
600 assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
601 assert!(index.cache_control.is_some());
602 let cache_control = index.cache_control.as_ref().unwrap();
603 assert_eq!(cache_control.api.as_deref(), Some("max-age=300"));
604 assert_eq!(cache_control.files, None);
605 }
606}