1use eyre::Result;
2use regex::Regex;
3use url::Url;
4
5#[derive(Debug, PartialEq, Eq)]
6pub struct RepoIssue {
7 pub owner: String,
8 pub repo: String,
9 pub issue_number: u64,
10}
11
12impl RepoIssue {
13 pub fn parse(input: &str) -> Result<Self> {
20 if let Ok(url) = Url::parse(input) {
22 return Self::from_url(&url);
23 }
24
25 if input.starts_with("github.com/") {
27 if let Ok(url) = Url::parse(&format!("https://{input}")) {
28 return Self::from_url(&url);
29 }
30 }
31
32 Self::from_path(input)
34 }
35
36 fn from_url(url: &Url) -> Result<Self> {
37 let path = url.path().trim_start_matches('/');
38 Self::from_path(path)
39 }
40
41 fn from_path(path: &str) -> Result<Self> {
42 let re = Regex::new(r"^([^/]+)/([^/#]+)(?:/(?:issues/)?|#)(\d+)$").unwrap();
47
48 if let Some(caps) = re.captures(path) {
49 let owner = caps[1].to_string();
50 let repo = caps[2].to_string();
51 let issue_number = caps[3].parse()?;
52
53 Ok(Self {
54 owner,
55 repo,
56 issue_number,
57 })
58 } else {
59 Err(eyre::eyre!("Invalid repository issue reference format"))
60 }
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_parse_full_url() {
70 let input = "https://github.com/ghbountybot/cli/issues/2";
71 let result = RepoIssue::parse(input).unwrap();
72 assert_eq!(
73 result,
74 RepoIssue {
75 owner: "ghbountybot".to_string(),
76 repo: "cli".to_string(),
77 issue_number: 2
78 }
79 );
80 }
81
82 #[test]
83 fn test_parse_domain_url() {
84 let input = "github.com/ghbountybot/cli/issues/2";
85 let result = RepoIssue::parse(input).unwrap();
86 assert_eq!(
87 result,
88 RepoIssue {
89 owner: "ghbountybot".to_string(),
90 repo: "cli".to_string(),
91 issue_number: 2
92 }
93 );
94 }
95
96 #[test]
97 fn test_parse_path_only() {
98 let input = "ghbountybot/cli/issues/2";
99 let result = RepoIssue::parse(input).unwrap();
100 assert_eq!(
101 result,
102 RepoIssue {
103 owner: "ghbountybot".to_string(),
104 repo: "cli".to_string(),
105 issue_number: 2
106 }
107 );
108 }
109
110 #[test]
111 fn test_parse_short_form() {
112 let input = "ghbountybot/cli/2";
113 let result = RepoIssue::parse(input).unwrap();
114 assert_eq!(
115 result,
116 RepoIssue {
117 owner: "ghbountybot".to_string(),
118 repo: "cli".to_string(),
119 issue_number: 2
120 }
121 );
122 }
123
124 #[test]
125 fn test_parse_issue_reference() {
126 let input = "ghbountybot/cli#2";
127 let result = RepoIssue::parse(input).unwrap();
128 assert_eq!(
129 result,
130 RepoIssue {
131 owner: "ghbountybot".to_string(),
132 repo: "cli".to_string(),
133 issue_number: 2
134 }
135 );
136 }
137
138 #[test]
139 fn test_parse_invalid_input() {
140 let inputs = [
141 "not-a-url",
142 "ghbountybot",
143 "ghbountybot/",
144 "ghbountybot/cli",
145 "ghbountybot/cli/",
146 "ghbountybot/cli/issues",
147 "ghbountybot/cli/issues/",
148 "ghbountybot/cli/issues/abc",
149 ];
150
151 for input in inputs {
152 assert!(
153 RepoIssue::parse(input).is_err(),
154 "Expected error for input: {input}"
155 );
156 }
157 }
158}