rootle_manager/
reference.rs1use super::{ManagerError, Result};
5
6#[derive(Debug, Clone, PartialEq)]
11pub struct ProviderReference {
12 pub repo: String,
14 pub name: String,
17 pub tag: Option<String>,
18 pub tarball: Option<String>,
22}
23
24impl ProviderReference {
25 pub fn parse(input: &str) -> Result<ProviderReference> {
26 let input = input.trim();
27 if input.is_empty() {
28 return Err(ManagerError::User("empty provider reference".into()));
29 }
30 let (path, tag) = match input.split_once('@') {
31 Some((p, t)) => (p, Some(t.to_string())),
32 None => (input, None),
33 };
34 let github = path
40 .strip_prefix("https://github.com/")
41 .or_else(|| path.strip_prefix("http://github.com/"));
42 if github.is_none() && (path.starts_with("https://") || path.starts_with("http://")) {
43 return parse_tarball_url(path, tag);
44 }
45 let repo = if let Some(rest) = github {
46 rest.trim_end_matches('/')
47 .trim_end_matches(".git")
48 .to_string()
49 } else {
50 path.to_string()
51 };
52 let has_slash = repo.contains('/');
53 let short = if has_slash {
54 let stem = repo.rsplit('/').next().unwrap_or(&repo).to_string();
55 stem.strip_prefix("rootle-").unwrap_or(&stem).to_string()
56 } else {
57 repo.clone()
58 };
59 let repo = if has_slash {
60 repo
61 } else {
62 format!("rootledev/rootle-{repo}")
64 };
65 let Some((owner, name)) = repo.split_once('/') else {
66 return Err(ManagerError::User(format!(
67 "provider reference {input:?} must be owner/repo, a GitHub URL, or a bare name"
68 )));
69 };
70 if owner.is_empty() || name.is_empty() {
71 return Err(ManagerError::User(format!("malformed reference {input:?}")));
72 }
73 Ok(ProviderReference {
74 repo,
75 name: short,
76 tag,
77 tarball: None,
78 })
79 }
80}
81
82fn parse_tarball_url(url: &str, tag: Option<String>) -> Result<ProviderReference> {
88 let file = url.rsplit('/').next().unwrap_or_default();
89 let target = super::release::platform_target();
90 let suffix = format!("-{target}.tar.gz");
91 let stem = file.strip_suffix(&suffix).ok_or_else(|| {
92 ManagerError::User(format!(
93 "plain-HTTP install needs the platform tarball: a URL ending in {suffix}"
94 ))
95 })?;
96 let stem = stem.strip_prefix("rootle-").unwrap_or(stem);
97 let (name, derived) = match stem.rsplit_once('-') {
98 Some((n, v)) if is_versionish(v) => (n, Some(normalize_tag(v))),
99 _ => (stem, None),
100 };
101 if name.is_empty() {
102 return Err(ManagerError::User(format!(
103 "cannot derive a provider name from {url:?}"
104 )));
105 }
106 Ok(ProviderReference {
107 repo: url.to_string(),
108 name: name.to_string(),
109 tag: tag.or(derived),
110 tarball: Some(url.to_string()),
111 })
112}
113
114fn is_versionish(s: &str) -> bool {
116 let s = s.strip_prefix('v').unwrap_or(s);
117 !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit() || b == b'.')
118}
119
120fn normalize_tag(v: &str) -> String {
122 if v.starts_with('v') {
123 v.to_string()
124 } else {
125 format!("v{v}")
126 }
127}
128
129pub(super) fn binary_name_of(r: &ProviderReference) -> String {
131 format!("rootle-{}", r.name)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn ref_grammar() {
140 assert_eq!(
142 ProviderReference::parse("gitlab").unwrap(),
143 ProviderReference {
144 repo: "rootledev/rootle-gitlab".into(),
145 name: "gitlab".into(),
146 tag: None,
147 tarball: None
148 }
149 );
150 assert_eq!(
152 ProviderReference::parse("rootledev/rootle-gitlab").unwrap(),
153 ProviderReference {
154 repo: "rootledev/rootle-gitlab".into(),
155 name: "gitlab".into(),
156 tag: None,
157 tarball: None
158 }
159 );
160 assert_eq!(
162 ProviderReference::parse("https://github.com/rootledev/rootle-gitlab").unwrap(),
163 ProviderReference {
164 repo: "rootledev/rootle-gitlab".into(),
165 name: "gitlab".into(),
166 tag: None,
167 tarball: None
168 }
169 );
170 assert_eq!(
172 ProviderReference::parse("rootledev/rootle-gitlab@v0.1.0")
173 .unwrap()
174 .tag,
175 Some("v0.1.0".into())
176 );
177 assert_eq!(
179 ProviderReference::parse("gitlab@v0.2.0").unwrap().tag,
180 Some("v0.2.0".into())
181 );
182 assert!(ProviderReference::parse("").is_err());
184 assert!(ProviderReference::parse("justtext/").is_err());
185 assert_eq!(
187 ProviderReference::parse("someone/myprovider").unwrap().name,
188 "myprovider"
189 );
190 }
191
192 #[test]
193 fn plain_http_tarball_urls() {
194 let target = super::super::release::platform_target();
195 let url =
196 format!("https://artifacts.corp.example/providers/rootle-gitlab-0.1.0-{target}.tar.gz");
197 let r = ProviderReference::parse(&url).unwrap();
198 assert_eq!(r.name, "gitlab");
199 assert_eq!(r.tag, Some("v0.1.0".into()));
200 assert_eq!(r.tarball, Some(url.clone()));
201 assert_eq!(r.repo, url);
203
204 let bare = format!("https://artifacts.corp.example/p/rootle-gitlab-{target}.tar.gz");
206 let r = ProviderReference::parse(&bare).unwrap();
207 assert_eq!((r.name.as_str(), r.tag), ("gitlab", None));
208
209 let pinned = ProviderReference::parse(&format!("{url}@v9.9.9")).unwrap();
211 assert_eq!(pinned.tag, Some("v9.9.9".into()));
212
213 assert!(
215 ProviderReference::parse("https://artifacts.corp.example/rootle-gitlab.tar.gz")
216 .is_err()
217 );
218 assert!(ProviderReference::parse("https://artifacts.corp.example/rootle-gitlab").is_err());
219 }
220}