update_kit/checker/sources/
jsr.rs1use crate::utils::http::{fetch_with_timeout, FetchOptions as HttpFetchOptions};
2
3use super::{FetchOptions, VersionInfo, VersionSource, VersionSourceResult};
4
5pub struct JsrSource {
7 scope: String,
8 name: String,
9 base_url: String,
10}
11
12impl JsrSource {
13 pub fn new(scope: String, name: String) -> Self {
14 Self {
15 scope,
16 name,
17 base_url: "https://jsr.io".to_string(),
18 }
19 }
20
21 pub fn with_base_url(scope: String, name: String, base_url: String) -> Self {
22 Self {
23 scope,
24 name,
25 base_url,
26 }
27 }
28}
29
30#[async_trait::async_trait]
31impl VersionSource for JsrSource {
32 fn name(&self) -> &str {
33 "jsr"
34 }
35
36 async fn fetch_latest(&self, _options: FetchOptions) -> VersionSourceResult {
37 let url = format!("{}/@{}/{}/meta.json", self.base_url, self.scope, self.name);
38
39 let response = match fetch_with_timeout(
40 &url,
41 Some(HttpFetchOptions {
42 timeout_ms: None,
43 headers: None,
44 }),
45 )
46 .await
47 {
48 Ok(r) => r,
49 Err(e) => {
50 return VersionSourceResult::Error {
51 reason: e.to_string(),
52 status: None,
53 }
54 }
55 };
56
57 let status = response.status().as_u16();
58
59 if !response.status().is_success() {
60 return VersionSourceResult::Error {
61 reason: format!("JSR returned status {}", status),
62 status: Some(status),
63 };
64 }
65
66 let json: serde_json::Value = match response.json().await {
67 Ok(j) => j,
68 Err(e) => {
69 return VersionSourceResult::Error {
70 reason: format!("Failed to parse response: {}", e),
71 status: Some(status),
72 }
73 }
74 };
75
76 let version = json
78 .get("latest")
79 .and_then(|v| v.as_str())
80 .map(String::from)
81 .or_else(|| {
82 json.get("versions")
84 .and_then(|v| v.as_object())
85 .and_then(|obj| obj.keys().next_back().cloned())
86 });
87
88 match version {
89 Some(version) => VersionSourceResult::Found {
90 info: VersionInfo {
91 version,
92 release_url: Some(format!(
93 "{}/@{}/{}",
94 self.base_url, self.scope, self.name
95 )),
96 release_notes: None,
97 assets: None,
98 published_at: None,
99 },
100 etag: None,
101 },
102 None => VersionSourceResult::Error {
103 reason: "Could not determine latest version from JSR meta.json".into(),
104 status: Some(status),
105 },
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_source_name() {
116 let source = JsrSource::new("scope".into(), "pkg".into());
117 assert_eq!(source.name(), "jsr");
118 }
119
120 #[test]
121 fn default_base_url() {
122 let source = JsrSource::new("scope".into(), "pkg".into());
123 assert_eq!(source.base_url, "https://jsr.io");
124 }
125
126 #[test]
127 fn custom_base_url() {
128 let source = JsrSource::with_base_url(
129 "scope".into(),
130 "pkg".into(),
131 "https://custom.jsr.io".into(),
132 );
133 assert_eq!(source.base_url, "https://custom.jsr.io");
134 }
135
136 #[tokio::test]
137 async fn fetch_latest_unreachable_returns_error() {
138 let source = JsrSource::with_base_url(
139 "scope".into(),
140 "pkg".into(),
141 "https://localhost:1".into(),
142 );
143 let result = source.fetch_latest(FetchOptions::default()).await;
144 match result {
145 VersionSourceResult::Error { reason, .. } => {
146 assert!(!reason.is_empty());
147 }
148 other => panic!("Expected Error, got: {other:?}"),
149 }
150 }
151
152 #[tokio::test]
153 async fn fetch_latest_http_rejected() {
154 let source = JsrSource::with_base_url(
155 "scope".into(),
156 "pkg".into(),
157 "http://insecure.com".into(),
158 );
159 let result = source.fetch_latest(FetchOptions::default()).await;
160 match result {
161 VersionSourceResult::Error { reason, .. } => {
162 assert!(
163 reason.contains("HTTPS") || reason.contains("Insecure"),
164 "Expected HTTPS/Insecure error, got: {reason}"
165 );
166 }
167 other => panic!("Expected Error for HTTP, got: {other:?}"),
168 }
169 }
170
171 #[tokio::test]
172 async fn fetch_versions_returns_unsupported() {
173 let source = JsrSource::new("scope".into(), "pkg".into());
174 let result = source
175 .fetch_versions(super::super::FetchVersionsOptions::default())
176 .await;
177 assert!(result.is_err());
178 let err = result.unwrap_err();
179 assert_eq!(err.code(), "UNSUPPORTED_OPERATION");
180 }
181
182 #[test]
183 fn source_name_is_jsr() {
184 let source = JsrSource::with_base_url("s".into(), "n".into(), "https://x.com".into());
185 assert_eq!(source.name(), "jsr");
186 }
187
188 #[test]
189 fn scope_and_name_stored() {
190 let source = JsrSource::new("my-scope".into(), "my-pkg".into());
191 assert_eq!(source.scope, "my-scope");
192 assert_eq!(source.name, "my-pkg");
193 }
194}