1use crate::proxy::Proxy;
2use strum_macros::EnumIter;
3
4#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
6#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
7#[derive(EnumIter, Debug, PartialEq, Hash, Eq, Clone)]
8pub enum Resource {
9 File {
13 owner: String,
14 repo: String,
15 reference: String,
16 path: String,
17 },
18 Release {
21 owner: String,
22 repo: String,
23 tag: String,
24 name: String,
25 },
26}
27
28impl Resource {
29 pub fn file(owner: String, repo: String, reference: String, path: String) -> Self {
37 Resource::File {
38 owner,
39 repo,
40 reference,
41 path,
42 }
43 }
44
45 pub fn release(owner: String, repo: String, tag: String, name: String) -> Self {
47 Resource::Release {
48 owner,
49 repo,
50 tag,
51 name,
52 }
53 }
54
55 pub fn url(&self, proxy_type: &Proxy) -> Option<String> {
60 match self {
61 Resource::File {
62 owner,
63 repo,
64 reference,
65 path,
66 } => Some(match proxy_type {
67 Proxy::Github => {
68 format!(
69 "https://github.com/{}/{}/raw/{}/{}",
70 owner, repo, reference, path
71 )
72 }
73 Proxy::Xget => {
74 format!(
75 "https://xget.xi-xu.me/gh/{}/{}/raw/{}/{}",
76 owner, repo, reference, path
77 )
78 }
79 Proxy::GhProxy => {
80 format!(
81 "https://gh-proxy.com/https://github.com/{}/{}/raw/{}/{}",
82 owner, repo, reference, path
83 )
84 }
85 Proxy::Jsdelivr => {
86 format!(
87 "https://cdn.jsdelivr.net/gh/{}/{}@{}/{}",
88 owner, repo, reference, path
89 )
90 }
91 Proxy::Statically => {
92 format!(
93 "https://cdn.statically.io/gh/{}/{}/{}/{}",
94 owner, repo, reference, path
95 )
96 }
97 }),
98 Resource::Release {
99 owner,
100 repo,
101 tag,
102 name,
103 } => match proxy_type {
104 Proxy::Github => Some(format!(
105 "https://github.com/{}/{}/releases/download/{}/{}",
106 owner, repo, tag, name
107 )),
108 Proxy::Xget => Some(format!(
109 "https://xget.xi-xu.me/gh/{}/{}/releases/download/{}/{}",
110 owner, repo, tag, name
111 )),
112 Proxy::GhProxy => Some(format!(
113 "https://gh-proxy.com/https://github.com/{}/{}/releases/download/{}/{}",
114 owner, repo, tag, name
115 )),
116 Proxy::Jsdelivr => None,
118 Proxy::Statically => None,
120 },
121 }
122 }
123}
124
125use crate::error::ConversionError;
126use regex::Regex;
127use std::sync::OnceLock;
128
129fn raw_file_regex() -> &'static Regex {
131 static RE: OnceLock<Regex> = OnceLock::new();
132 RE.get_or_init(|| {
133 Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/raw/(?P<rest>.+)$")
135 .unwrap()
136 })
137}
138
139fn blob_file_regex() -> &'static Regex {
140 static RE: OnceLock<Regex> = OnceLock::new();
141 RE.get_or_init(|| {
142 Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/blob/(?P<rest>.+)$")
144 .unwrap()
145 })
146}
147
148fn release_download_regex() -> &'static Regex {
149 static RE: OnceLock<Regex> = OnceLock::new();
150 RE.get_or_init(|| {
151 Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/releases/download/(?P<tag>[^/]+)/(?P<filename>.+)$")
152 .unwrap()
153 })
154}
155
156impl TryFrom<&str> for Resource {
157 type Error = ConversionError;
158
159 fn try_from(value: &str) -> Result<Self, Self::Error> {
160 let value = value.trim();
161
162 if let Some(captures) = raw_file_regex().captures(value) {
164 let owner = captures["owner"].to_string();
165 let repo = captures["repo"].to_string();
166 let rest = &captures["rest"];
167
168 let (reference, path) = split_reference_and_path(rest)?;
173
174 return Ok(Resource::File {
175 owner,
176 repo,
177 reference,
178 path,
179 });
180 }
181
182 if let Some(captures) = blob_file_regex().captures(value) {
184 let owner = captures["owner"].to_string();
185 let repo = captures["repo"].to_string();
186 let rest = &captures["rest"];
187
188 let (reference, path) = split_reference_and_path(rest)?;
189
190 return Ok(Resource::File {
191 owner,
192 repo,
193 reference,
194 path,
195 });
196 }
197
198 if let Some(captures) = release_download_regex().captures(value) {
200 return Ok(Resource::Release {
201 owner: captures["owner"].to_string(),
202 repo: captures["repo"].to_string(),
203 tag: captures["tag"].to_string(),
204 name: captures["filename"].to_string(),
205 });
206 }
207
208 Err(ConversionError::InvalidUrl(value.to_string()))
209 }
210}
211
212fn split_reference_and_path(rest: &str) -> Result<(String, String), ConversionError> {
218 let parts: Vec<&str> = rest.split('/').collect();
219
220 if parts.is_empty() {
221 return Err(ConversionError::ParseError(
222 "Missing reference and path".to_string(),
223 ));
224 }
225
226 if parts.len() >= 4 && parts[0] == "refs" {
228 let reference = format!("{}/{}/{}", parts[0], parts[1], parts[2]);
230 let path = parts[3..].join("/");
231
232 if path.is_empty() {
233 return Err(ConversionError::ParseError("Missing file path".to_string()));
234 }
235
236 Ok((reference, path))
237 } else if parts.len() >= 2 {
238 let reference = parts[0].to_string();
240 let path = parts[1..].join("/");
241 Ok((reference, path))
242 } else {
243 Err(ConversionError::ParseError(
244 "Invalid reference/path format".to_string(),
245 ))
246 }
247}
248
249impl TryFrom<String> for Resource {
250 type Error = ConversionError;
251
252 fn try_from(value: String) -> Result<Self, Self::Error> {
253 Resource::try_from(value.as_str())
254 }
255}