update_kit/checker/sources/
custom_manifest.rs1use crate::utils::http::{fetch_with_timeout, FetchOptions as HttpFetchOptions};
2
3use super::{FetchOptions, VersionInfo, VersionSource, VersionSourceResult};
4
5pub struct CustomManifestSource {
7 url: String,
8 version_field: String,
9}
10
11impl CustomManifestSource {
12 pub fn new(url: String, version_field: Option<String>) -> Self {
13 Self {
14 url,
15 version_field: version_field.unwrap_or_else(|| "version".to_string()),
16 }
17 }
18
19 fn extract_field<'a>(json: &'a serde_json::Value, field_path: &str) -> Option<&'a str> {
22 let mut current = json;
23 for part in field_path.split('.') {
24 current = current.get(part)?;
25 }
26 current.as_str()
27 }
28}
29
30#[async_trait::async_trait]
31impl VersionSource for CustomManifestSource {
32 fn name(&self) -> &str {
33 "custom"
34 }
35
36 async fn fetch_latest(&self, _options: FetchOptions) -> VersionSourceResult {
37 let response = match fetch_with_timeout(
38 &self.url,
39 Some(HttpFetchOptions {
40 timeout_ms: None,
41 headers: None,
42 }),
43 )
44 .await
45 {
46 Ok(r) => r,
47 Err(e) => {
48 return VersionSourceResult::Error {
49 reason: e.to_string(),
50 status: None,
51 }
52 }
53 };
54
55 let status = response.status().as_u16();
56
57 if !response.status().is_success() {
58 return VersionSourceResult::Error {
59 reason: format!("Custom manifest returned status {}", status),
60 status: Some(status),
61 };
62 }
63
64 let json: serde_json::Value = match response.json().await {
65 Ok(j) => j,
66 Err(e) => {
67 return VersionSourceResult::Error {
68 reason: format!("Failed to parse response: {}", e),
69 status: Some(status),
70 }
71 }
72 };
73
74 match Self::extract_field(&json, &self.version_field) {
75 Some(version) => VersionSourceResult::Found {
76 info: VersionInfo {
77 version: version.to_string(),
78 release_url: None,
79 release_notes: None,
80 assets: None,
81 published_at: None,
82 },
83 etag: None,
84 },
85 None => VersionSourceResult::Error {
86 reason: format!(
87 "Field '{}' not found in manifest response",
88 self.version_field
89 ),
90 status: Some(status),
91 },
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_source_name() {
102 let source = CustomManifestSource::new(
103 "https://example.com/version.json".into(),
104 None,
105 );
106 assert_eq!(source.name(), "custom");
107 }
108
109 #[test]
110 fn test_default_version_field() {
111 let source = CustomManifestSource::new(
112 "https://example.com/version.json".into(),
113 None,
114 );
115 assert_eq!(source.version_field, "version");
116 }
117
118 #[test]
119 fn test_extract_field_simple() {
120 let json = serde_json::json!({"version": "1.2.3"});
121 assert_eq!(
122 CustomManifestSource::extract_field(&json, "version"),
123 Some("1.2.3")
124 );
125 }
126
127 #[test]
128 fn test_extract_field_nested() {
129 let json = serde_json::json!({
130 "data": {
131 "latest": {
132 "version": "2.0.0"
133 }
134 }
135 });
136 assert_eq!(
137 CustomManifestSource::extract_field(&json, "data.latest.version"),
138 Some("2.0.0")
139 );
140 }
141
142 #[test]
143 fn test_extract_field_missing() {
144 let json = serde_json::json!({"name": "app"});
145 assert_eq!(
146 CustomManifestSource::extract_field(&json, "version"),
147 None
148 );
149 }
150
151 #[test]
152 fn custom_version_field() {
153 let source = CustomManifestSource::new(
154 "https://example.com/v.json".into(),
155 Some("data.version".into()),
156 );
157 assert_eq!(source.version_field, "data.version");
158 }
159
160 #[test]
161 fn extract_field_deeply_nested() {
162 let json = serde_json::json!({"a": {"b": {"c": {"d": "1.0.0"}}}});
163 assert_eq!(
164 CustomManifestSource::extract_field(&json, "a.b.c.d"),
165 Some("1.0.0")
166 );
167 }
168
169 #[test]
170 fn extract_field_non_string_value() {
171 let json = serde_json::json!({"version": 123});
172 assert_eq!(
173 CustomManifestSource::extract_field(&json, "version"),
174 None
175 );
176 }
177
178 #[test]
179 fn extract_field_null_value() {
180 let json = serde_json::json!({"version": null});
181 assert_eq!(
182 CustomManifestSource::extract_field(&json, "version"),
183 None
184 );
185 }
186
187 #[test]
188 fn extract_field_array_value() {
189 let json = serde_json::json!({"version": ["1.0.0"]});
190 assert_eq!(
191 CustomManifestSource::extract_field(&json, "version"),
192 None
193 );
194 }
195
196 #[tokio::test]
197 async fn fetch_latest_http_url_rejected() {
198 let source =
199 CustomManifestSource::new("http://insecure.com/version.json".into(), None);
200 let result = source.fetch_latest(FetchOptions::default()).await;
201 match result {
202 VersionSourceResult::Error { reason, .. } => {
203 assert!(reason.contains("HTTPS") || reason.contains("Insecure"));
204 }
205 other => panic!("Expected Error, got: {other:?}"),
206 }
207 }
208
209 #[tokio::test]
210 async fn fetch_latest_unreachable_returns_error() {
211 let source =
212 CustomManifestSource::new("https://localhost:1/version.json".into(), None);
213 let result = source.fetch_latest(FetchOptions::default()).await;
214 match result {
215 VersionSourceResult::Error { reason, .. } => {
216 assert!(!reason.is_empty());
217 }
218 other => panic!("Expected Error, got: {other:?}"),
219 }
220 }
221
222 #[tokio::test]
223 async fn fetch_versions_returns_unsupported() {
224 let source =
225 CustomManifestSource::new("https://example.com/v.json".into(), None);
226 let result = source
227 .fetch_versions(super::super::FetchVersionsOptions::default())
228 .await;
229 assert!(result.is_err());
230 assert_eq!(result.unwrap_err().code(), "UNSUPPORTED_OPERATION");
231 }
232}