gh_trs/
raw_url.rs

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    /// Parse the workflow location.
24    /// The workflow location should be in the format of:
25    ///
26    /// - https://github.com/<owner>/<name>/blob/<branch>/<path_to_file>
27    /// - https://github.com/<owner>/<name>/blob/<commit_hash>/<path_to_file>
28    /// - https://github.com/<owner>/<name>/tree/<branch>/<path_to_file>
29    /// - https://github.com/<owner>/<name>/tree/<commit_hash>/<path_to_file>
30    /// - https://github.com/<owner>/<name>/raw/<branch>/<path_to_file>
31    /// - https://github.com/<owner>/<name>/raw/<commit_hash>/<path_to_file>
32    /// - https://raw.githubusercontent.com/<owner>/<name>/<branch>/<path_to_file>
33    /// - https://raw.githubusercontent.com/<owner>/<name>/<commit_hash>/<path_to_file>
34    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    // UrlType::Branch
129    // -> https://raw.githubusercontent.com/suecharo/gh-trs/main/README.md
130    // UrlType::Commit
131    // -> https://raw.githubusercontent.com/suecharo/gh-trs/<commit_hash>/README.md
132    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        // remove trailing slash
163        let path = path.trim_end_matches('/');
164        // need to add a trailing slash to make it a valid URL
165        let url = Url::parse(&format!(
166            "
167            https://raw.githubusercontent.com/{}/",
168            path
169        ))?;
170        Ok(url)
171    }
172}
173
174/// Check if input is a valid commit SHA.
175pub 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}