1use crate::github_api;
2use anyhow::{anyhow, ensure, Result};
3use regex::Regex;
4use std::collections::HashMap;
5use std::path::PathBuf;
6use url::Url;
7
8#[derive(Debug, PartialEq, Clone)]
9pub struct RawUrl {
10 pub owner: String,
11 pub name: String,
12 pub branch: String,
13 pub commit: String,
14 pub file_path: PathBuf,
15}
16
17pub enum UrlType {
18 Branch,
19 Commit,
20}
21
22impl RawUrl {
23 pub fn new(
35 gh_token: impl AsRef<str>,
36 url: &Url,
37 branch_memo: Option<&mut HashMap<String, String>>,
38 commit_memo: Option<&mut HashMap<String, String>>,
39 ) -> Result<Self> {
40 let host = url
41 .host_str()
42 .ok_or_else(|| anyhow!("No host found in URL: {}", url))?;
43 ensure!(
44 host == "github.com" || host == "raw.githubusercontent.com",
45 "Only GitHub URLs are supported, your input URL: {}",
46 url
47 );
48 let path_segments = url
49 .path_segments()
50 .ok_or_else(|| anyhow!("Failed to parse URL path: {}", url))?
51 .collect::<Vec<_>>();
52 let owner = path_segments
53 .get(0)
54 .ok_or_else(|| anyhow!("No repo owner found in URL: {}", url))?
55 .to_string();
56 let name = path_segments
57 .get(1)
58 .ok_or_else(|| anyhow!("No repo name found in URL: {}", url))?
59 .to_string();
60 let branch_or_commit = match host {
61 "github.com" => path_segments
62 .get(3)
63 .ok_or_else(|| anyhow!("No branch or commit found in URL: {}", url))?,
64 "raw.githubusercontent.com" => path_segments
65 .get(2)
66 .ok_or_else(|| anyhow!("No branch or commit found in URL: {}", url))?,
67 _ => unreachable!(),
68 };
69 let (branch, commit) = match is_commit_hash(&branch_or_commit) {
70 Ok(_) => {
71 let commit = branch_or_commit.to_string();
72 let branch = github_api::get_default_branch(gh_token, &owner, &name, branch_memo)?;
73 (branch, commit)
74 }
75 Err(_) => {
76 let branch = branch_or_commit.to_string();
77 let commit = github_api::get_latest_commit_sha(
78 gh_token,
79 &owner,
80 &name,
81 &branch,
82 commit_memo,
83 )?;
84 (branch, commit)
85 }
86 };
87 let file_path = match host {
88 "github.com" => PathBuf::from(path_segments[4..].join("/")),
89 "raw.githubusercontent.com" => PathBuf::from(path_segments[3..].join("/")),
90 _ => unreachable!(),
91 };
92 Ok(Self {
93 owner,
94 name,
95 branch,
96 commit,
97 file_path,
98 })
99 }
100
101 pub fn file_stem(&self) -> Result<String> {
102 Ok(self
103 .file_path
104 .file_stem()
105 .ok_or_else(|| {
106 anyhow!(
107 "Failed to get file stem from {} ",
108 self.file_path.to_string_lossy()
109 )
110 })?
111 .to_string_lossy()
112 .to_string())
113 }
114
115 pub fn base_dir(&self) -> Result<PathBuf> {
116 Ok(self
117 .file_path
118 .parent()
119 .ok_or_else(|| {
120 anyhow!(
121 "Failed to get parent dir from {} ",
122 self.file_path.to_string_lossy()
123 )
124 })?
125 .to_path_buf())
126 }
127
128 pub fn to_url(&self, url_type: &UrlType) -> Result<Url> {
133 Ok(Url::parse(&format!(
134 "https://raw.githubusercontent.com/{}/{}/{}/{}",
135 self.owner,
136 self.name,
137 match url_type {
138 UrlType::Branch => &self.branch,
139 UrlType::Commit => &self.commit,
140 },
141 self.file_path.to_string_lossy()
142 ))?)
143 }
144
145 pub fn to_base_url(&self, url_type: &UrlType) -> Result<Url> {
146 let path = format!(
147 "{}/{}/{}/{}",
148 self.owner,
149 self.name,
150 match url_type {
151 UrlType::Branch => &self.branch,
152 UrlType::Commit => &self.commit,
153 },
154 self.file_path
155 .parent()
156 .ok_or_else(|| anyhow!(
157 "Failed to get parent dir from {}",
158 self.file_path.to_string_lossy()
159 ))?
160 .to_string_lossy()
161 );
162 let path = path.trim_end_matches('/');
164 let url = Url::parse(&format!(
166 "
167 https://raw.githubusercontent.com/{}/",
168 path
169 ))?;
170 Ok(url)
171 }
172}
173
174pub fn is_commit_hash(hash: impl AsRef<str>) -> Result<()> {
176 let re = Regex::new(r"^[0-9a-f]{40}$")?;
177 ensure!(
178 re.is_match(hash.as_ref()),
179 "Not a valid commit hash: {}",
180 hash.as_ref()
181 );
182 Ok(())
183}
184
185#[cfg(test)]
186#[cfg(not(tarpaulin_include))]
187mod tests {
188 use super::*;
189 use crate::env;
190
191 #[test]
192 fn test_raw_url() -> Result<()> {
193 let gh_token = env::github_token(&None::<String>)?;
194 let owner = "suecharo".to_string();
195 let name = "gh-trs".to_string();
196 let branch = "main".to_string();
197 let commit = "f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9".to_string();
198 let file_path = PathBuf::from("path/to/workflow.yml");
199
200 let url_1 = Url::parse(&format!(
201 "https://github.com/{}/{}/blob/{}/{}",
202 &owner,
203 &name,
204 &branch,
205 &file_path.to_string_lossy()
206 ))?;
207 let url_2 = Url::parse(&format!(
208 "https://github.com/{}/{}/blob/{}/{}",
209 &owner,
210 &name,
211 &commit,
212 &file_path.to_string_lossy()
213 ))?;
214 let url_3 = Url::parse(&format!(
215 "https://raw.githubusercontent.com/{}/{}/{}/{}",
216 &owner,
217 &name,
218 &branch,
219 &file_path.to_string_lossy()
220 ))?;
221 let url_4 = Url::parse(&format!(
222 "https://raw.githubusercontent.com/{}/{}/{}/{}",
223 &owner,
224 &name,
225 &commit,
226 &file_path.to_string_lossy()
227 ))?;
228
229 let raw_url_1 = RawUrl::new(&gh_token, &url_1, None, None)?;
230 let raw_url_2 = RawUrl::new(&gh_token, &url_2, None, None)?;
231 let raw_url_3 = RawUrl::new(&gh_token, &url_3, None, None)?;
232 let raw_url_4 = RawUrl::new(&gh_token, &url_4, None, None)?;
233
234 let expect = RawUrl {
235 owner,
236 name,
237 branch,
238 commit,
239 file_path,
240 };
241
242 assert_eq!(raw_url_1.owner, expect.owner);
243 assert_eq!(raw_url_1.name, expect.name);
244 assert_eq!(raw_url_1.branch, expect.branch);
245 assert_eq!(raw_url_1.file_path, expect.file_path);
246
247 assert_eq!(raw_url_2, expect);
248
249 assert_eq!(raw_url_3.owner, expect.owner);
250 assert_eq!(raw_url_3.name, expect.name);
251 assert_eq!(raw_url_3.branch, expect.branch);
252 assert_eq!(raw_url_3.file_path, expect.file_path);
253
254 assert_eq!(raw_url_4, expect);
255
256 Ok(())
257 }
258
259 #[test]
260 fn test_raw_url_invalid_url() -> Result<()> {
261 let gh_token = env::github_token(&None::<String>)?;
262 let url = Url::parse("https://example.com/path/to/file")?;
263 let err = RawUrl::new(&gh_token, &url, None, None).unwrap_err();
264 assert_eq!(
265 err.to_string(),
266 "Only GitHub URLs are supported, your input URL: https://example.com/path/to/file"
267 );
268 Ok(())
269 }
270
271 #[test]
272 fn test_raw_url_invalid_host() -> Result<()> {
273 let gh_token = env::github_token(&None::<String>)?;
274 let url = Url::parse("https://example.com/path/to/file")?;
275 let err = RawUrl::new(&gh_token, &url, None, None).unwrap_err();
276 assert_eq!(
277 err.to_string(),
278 "Only GitHub URLs are supported, your input URL: https://example.com/path/to/file"
279 );
280 Ok(())
281 }
282
283 #[test]
284 fn test_raw_url_invalid_path() -> Result<()> {
285 let gh_token = env::github_token(&None::<String>)?;
286 let url =
287 Url::parse("https://github.com/suecharo/gh-trs/blob/invalid_branch/path/to/workflow")?;
288 assert!(RawUrl::new(&gh_token, &url, None, None).is_err());
289 Ok(())
290 }
291
292 #[test]
293 fn test_is_commit_hash() -> Result<()> {
294 let commit = "f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9";
295 is_commit_hash(commit)?;
296 Ok(())
297 }
298
299 #[test]
300 fn test_base_dir() -> Result<()> {
301 let gh_token = env::github_token(&None::<String>)?;
302 let owner = "suecharo".to_string();
303 let name = "gh-trs".to_string();
304 let commit = "f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9".to_string();
305 let file_path = PathBuf::from("path/to/workflow.yml");
306 let url = Url::parse(&format!(
307 "https://github.com/{}/{}/blob/{}/{}",
308 &owner,
309 &name,
310 &commit,
311 &file_path.to_string_lossy()
312 ))?;
313 let raw_url = RawUrl::new(&gh_token, &url, None, None)?;
314 let base_dir = raw_url.base_dir()?;
315 assert_eq!(base_dir, PathBuf::from("path/to"));
316 Ok(())
317 }
318
319 #[test]
320 fn test_to_url() -> Result<()> {
321 let gh_token = env::github_token(&None::<String>)?;
322 let owner = "suecharo".to_string();
323 let name = "gh-trs".to_string();
324 let commit = "f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9".to_string();
325 let file_path = PathBuf::from("path/to/workflow.yml");
326 let url = Url::parse(&format!(
327 "https://github.com/{}/{}/blob/{}/{}",
328 &owner,
329 &name,
330 &commit,
331 &file_path.to_string_lossy()
332 ))?;
333 let raw_url = RawUrl::new(&gh_token, &url, None, None)?;
334 let to_url = raw_url.to_url(&UrlType::Commit)?;
335 assert_eq!(
336 to_url,
337 Url::parse(&format!(
338 "https://raw.githubusercontent.com/{}/{}/{}/{}",
339 &owner,
340 &name,
341 &commit,
342 &file_path.to_string_lossy()
343 ))?
344 );
345 Ok(())
346 }
347
348 #[test]
349 fn test_to_base_url() -> Result<()> {
350 let gh_token = env::github_token(&None::<String>)?;
351 let owner = "suecharo".to_string();
352 let name = "gh-trs".to_string();
353 let commit = "f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9".to_string();
354 let file_path = PathBuf::from("path/to/workflow.yml");
355 let url = Url::parse(&format!(
356 "https://github.com/{}/{}/blob/{}/{}",
357 &owner,
358 &name,
359 &commit,
360 &file_path.to_string_lossy()
361 ))?;
362 let raw_url = RawUrl::new(&gh_token, &url, None, None)?;
363 let to_url = raw_url.to_base_url(&UrlType::Commit)?;
364 assert_eq!(
365 to_url,
366 Url::parse(&format!(
367 "https://raw.githubusercontent.com/{}/{}/{}/path/to/",
368 &owner, &name, &commit,
369 ))?
370 );
371 Ok(())
372 }
373}