uv_pypi_types/
direct_url.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
6
7/// Metadata for a distribution that was installed via a direct URL.
8///
9/// See: <https://packaging.python.org/en/latest/specifications/direct-url-data-structure/>
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", untagged)]
12pub enum DirectUrl {
13    /// The direct URL is a local directory. For example:
14    /// ```json
15    /// {"url": "file:///home/user/project", "dir_info": {}}
16    /// ```
17    LocalDirectory {
18        url: String,
19        dir_info: DirInfo,
20        #[serde(skip_serializing_if = "Option::is_none")]
21        subdirectory: Option<Box<Path>>,
22    },
23    /// The direct URL is a path to an archive. For example:
24    /// ```json
25    /// {"archive_info": {"hash": "sha256=75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8", "hashes": {"sha256": "75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"}}, "url": "https://files.pythonhosted.org/packages/b8/8b/31273bf66016be6ad22bb7345c37ff350276cfd46e389a0c2ac5da9d9073/wheel-0.41.2-py3-none-any.whl"}
26    /// ```
27    ArchiveUrl {
28        /// The URL without parsed information (such as the Git revision or subdirectory).
29        ///
30        /// For example, for `pip install git+https://github.com/tqdm/tqdm@cc372d09dcd5a5eabdc6ed4cf365bdb0be004d44#subdirectory=.`,
31        /// the URL is `https://github.com/tqdm/tqdm`.
32        url: String,
33        archive_info: ArchiveInfo,
34        #[serde(skip_serializing_if = "Option::is_none")]
35        subdirectory: Option<Box<Path>>,
36    },
37    /// The direct URL is path to a VCS repository. For example:
38    /// ```json
39    /// {"url": "https://github.com/pallets/flask.git", "vcs_info": {"commit_id": "8d9519df093864ff90ca446d4af2dc8facd3c542", "vcs": "git", "git_lfs": true }}
40    /// ```
41    VcsUrl {
42        url: String,
43        vcs_info: VcsInfo,
44        #[serde(skip_serializing_if = "Option::is_none")]
45        subdirectory: Option<Box<Path>>,
46    },
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub struct DirInfo {
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub editable: Option<bool>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub struct ArchiveInfo {
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub hash: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub hashes: Option<BTreeMap<String, String>>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub struct VcsInfo {
68    pub vcs: VcsKind,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub commit_id: Option<String>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub requested_revision: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub git_lfs: Option<bool>, // Prefix lfs with VcsKind::Git per PEP 610
75}
76
77#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum VcsKind {
80    Git,
81    Hg,
82    Bzr,
83    Svn,
84}
85
86impl std::fmt::Display for VcsKind {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::Git => write!(f, "git"),
90            Self::Hg => write!(f, "hg"),
91            Self::Bzr => write!(f, "bzr"),
92            Self::Svn => write!(f, "svn"),
93        }
94    }
95}
96
97impl TryFrom<&DirectUrl> for DisplaySafeUrl {
98    type Error = DisplaySafeUrlError;
99
100    fn try_from(value: &DirectUrl) -> Result<Self, Self::Error> {
101        match value {
102            DirectUrl::LocalDirectory {
103                url,
104                subdirectory,
105                dir_info: _,
106            } => {
107                let mut url = Self::parse(url)?;
108                if let Some(subdirectory) = subdirectory {
109                    url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
110                }
111                Ok(url)
112            }
113            DirectUrl::ArchiveUrl {
114                url,
115                subdirectory,
116                archive_info: _,
117            } => {
118                let mut url = Self::parse(url)?;
119                if let Some(subdirectory) = subdirectory {
120                    url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
121                }
122                Ok(url)
123            }
124            DirectUrl::VcsUrl {
125                url,
126                vcs_info,
127                subdirectory,
128            } => {
129                let mut url = Self::parse(&format!("{}+{}", vcs_info.vcs, url))?;
130                if let Some(commit_id) = &vcs_info.commit_id {
131                    let path = format!("{}@{commit_id}", url.path());
132                    url.set_path(&path);
133                } else if let Some(requested_revision) = &vcs_info.requested_revision {
134                    let path = format!("{}@{requested_revision}", url.path());
135                    url.set_path(&path);
136                }
137                let mut frags: Vec<String> = Vec::new();
138                if let Some(subdirectory) = subdirectory {
139                    frags.push(format!("subdirectory={}", subdirectory.display()));
140                }
141                // Displays nicely that lfs was used
142                if let Some(true) = vcs_info.git_lfs {
143                    frags.push("lfs=true".to_string());
144                }
145                if !frags.is_empty() {
146                    url.set_fragment(Some(&frags.join("&")));
147                }
148                Ok(url)
149            }
150        }
151    }
152}