rust_jsr_registry/fetcher.rs
1use anyhow::Result;
2use reqwest::{Client, StatusCode};
3use serde::de::DeserializeOwned;
4use url::Url;
5
6use crate::{meta::{Meta, MetaBuilder, NpmCompMeta}, package::{Package, PackageBuilder}, priv_as_ref, priv_impl_default, DEFAULT_NPM_COMP_URL, DEFAULT_URL};
7pub trait GetProviderScope {
8 fn get_provider_scope(&self) -> &str;
9}
10#[derive(Debug, Clone)]
11pub struct FetcherBuilder {
12 /// The host
13 pub host: Host,
14 /// The [reqwest] [Client] to be used in Fetcher
15 pub client: Option<Client>,
16 /// The jsr provider scope on npm-side. Defaults to `jsr`
17 ///
18 /// NOTE: This is **DIFFERENT** than normal package scope
19 ///
20 /// See https://jsr.io/docs/api#npm-compatibility-registry-api
21 pub provider_scope: String,
22}
23impl GetProviderScope for FetcherBuilder {
24 fn get_provider_scope(&self) -> &str {
25 return self.provider_scope.as_str();
26 }
27}
28impl FetcherBuilder {
29 /// Creates a builder for [Fetcher]
30 pub fn new() -> Self {
31 return Self {
32 host: Host::new(),
33 client: None,
34 provider_scope: "jsr".to_string()
35 };
36 }
37 /// Set the host urls
38 pub fn set_host(mut self, value:impl AsRef<Host>) -> Self {
39 self.host = *value.as_ref();
40 return self;
41 }
42 /// Set the [reqwest] [Client]
43 ///
44 /// ## Performance cost
45 ///
46 /// Since client was intentionally cloned ([see here](https://docs.rs/reqwest/0.11.4/src/reqwest/async_impl/client.rs.html#61)),
47 /// It doesn't impact the performance
48 pub fn set_client(mut self, value:impl AsRef<Client>) -> Self {
49 self.client = Some(value.as_ref().clone());
50 return self;
51 }
52 /// Set the provider scope (for npm-side)
53 pub fn set_provider_scope(mut self, value:impl AsRef<String>) -> Self {
54 self.provider_scope = value.as_ref().to_string();
55 return self;
56 }
57}
58priv_impl_default!(FetcherBuilder);
59pub enum HostSelector {
60 Main,
61 NpmComp
62}
63/// Creates a host urls for [FetcherBuilder] and [Fetcher]
64#[derive(Debug, Clone, Copy)]
65pub struct Host {
66 pub main:&'static Url,
67 pub npm_comp:&'static Url,
68}
69impl Host {
70 /// Creates a host urls for [FetcherBuilder] and [Fetcher]
71 pub fn new() -> Self {
72 return Self {
73 main:&DEFAULT_URL,
74 npm_comp:&DEFAULT_NPM_COMP_URL
75 };
76 }
77 /// Set the host url for JSR main packages
78 pub fn set_main(mut self, value:&'static Url) -> Self {
79 self.main = value;
80 return self;
81 }
82 /// Set the host url for JSR-to-npm compatible packages
83 pub fn set_npm_comp(mut self, value:&'static Url) -> Self {
84 self.npm_comp = value;
85 return self;
86 }
87}
88priv_as_ref!(Host);
89priv_impl_default!(Host);
90#[derive(Debug)]
91pub struct Fetcher {
92 /// The host url (defaults to `https://jsr.io`)
93 pub host: Host,
94
95 /// The [reqwest] [Client] to be used in Fetcher
96 client: Client,
97
98 /// The jsr provider scope on npm-side. Defaults to `jsr`
99 ///
100 /// NOTE: This is **DIFFERENT** than normal package scope
101 ///
102 /// See https://jsr.io/docs/api#npm-compatibility-registry-api
103 pub provider_scope: String,
104}
105impl GetProviderScope for Fetcher {
106 fn get_provider_scope(&self) -> &str {
107 return self.provider_scope.as_str();
108 }
109}
110impl Fetcher {
111 /// Creates a new Fetcher
112 ///
113 /// ## Panics
114 ///
115 /// See the docs for [reqwest::Client::new]. You can prevent this by setting your own [reqwest] [Client] in [FetcherBuilder]
116 pub fn new(builder: FetcherBuilder) -> Self {
117 return Self {
118 host: builder.host,
119 client: if let Some(v) = builder.client {
120 v
121 } else {
122 Client::new()
123 },
124 provider_scope: builder.provider_scope
125 };
126 }
127 /// The fetcher.
128 ///
129 /// NOTE for [reqwest::Error]: The fetcher **always** hides the url, so its safe
130 ///
131 /// ## Errors
132 ///
133 /// Throws [reqwest::Error] or [serde_json::Error]
134 async fn fetcher<T: DeserializeOwned>(&self, sel:HostSelector, path: impl Into<String>) -> Result<Option<T>> {
135 let host = match sel {
136 HostSelector::Main => {
137 self.host.main
138 }
139 HostSelector::NpmComp => {
140 self.host.npm_comp
141 }
142 };
143 let url = format!("{}{}", host, path.into());
144 let res_raw = self.client.get(url).send().await;
145 if let Err(v) = res_raw {
146 return Err(v.without_url().into());
147 }
148
149 let res = res_raw.unwrap();
150
151 // Clone status before consuming `res`
152 let status = res.status();
153 if let Err(v) = res.error_for_status_ref() { // Use `error_for_status_ref()` instead
154 let err = v.without_url();
155 if status == StatusCode::NOT_FOUND {
156 return Ok(None);
157 } else {
158 return Err(err.into());
159 }
160 }
161
162 // Now `res` is still available, so we can read the body
163 let body = res.text().await;
164 match body {
165 Err(v) => Err(v.without_url().into()),
166 Ok(v) => {
167 let parsed: T = serde_json::from_str(&v)?; // Deserialize JSON
168 Ok(Some(parsed))
169 }
170 }
171 }
172
173 /// Get package metadata, contains version details, and more
174 ///
175 /// Returns `Ok(None)` if the server returned `404`.
176 /// If the other failed status code returned, `Err` will be returned.
177 /// Else, `Ok(Some(Meta))` returned normally as success
178 ///
179 /// See https://jsr.io/docs/api#package-version
180 ///
181 /// ## Errors
182 ///
183 /// Throws [reqwest::Error] or [serde_json::Error]
184 pub async fn get_meta(&self, value:impl AsRef<MetaBuilder>) -> Result<Option<Meta>> {
185 let res = value.as_ref();
186 return self
187 .fetcher::<Meta>(HostSelector::Main, format!("@{}/{}/meta.json", res.scope, res.name))
188 .await;
189 }
190 /// Get metadatas from packages, contains version details, and more
191 ///
192 /// See https://jsr.io/docs/api#package-version
193 ///
194 /// ## Errors
195 ///
196 /// Throws [reqwest::Error] or [serde_json::Error]
197 pub async fn get_metas<T: AsRef<MetaBuilder>, U: IntoIterator<Item = T> + ExactSizeIterator>(&self, values:U) -> Result<Vec<Meta>> {
198 let mut results = Vec::with_capacity(values.len());
199 for each in values {
200 let res = self.get_meta(each).await?;
201 if let Some(v) = res {
202 results.push(v);
203 }
204 }
205 Ok(results)
206 }
207 /// Get package **with** specific version on it.
208 ///
209 /// Returns `Ok(None)` if the server returned `404`.
210 /// If the other failed status code returned, `Err` will be returned.
211 /// Else, `Ok(Some(Package))` returned normally as success
212 ///
213 /// See https://jsr.io/docs/api#package-version-metadata
214 ///
215 /// To get the list of versions, use [Fetcher::get_meta]
216 ///
217 /// ## Errors
218 ///
219 /// Throws [reqwest::Error] or [serde_json::Error]
220 pub async fn get_package(&self, value: impl AsRef<PackageBuilder>) -> Result<Option<Package>> {
221 let res = value.as_ref();
222 return self
223 .fetcher::<Package>(HostSelector::Main, format!("@{}/{}/{}_meta.json", res.scope, res.name, res.version))
224 .await;
225 }
226 /// Get each packages **with** specific version on it
227 ///
228 /// See https://jsr.io/docs/api#package-version-metadata
229 ///
230 /// ## Errors
231 ///
232 /// Throws [reqwest::Error] or [serde_json::Error]
233 pub async fn get_packages<T: AsRef<PackageBuilder>, U: IntoIterator<Item = T> + ExactSizeIterator>(&self, values: U) -> Result<Vec<Package>> {
234 let mut results = Vec::with_capacity(values.len());
235 for each in values {
236 let res = self.get_package(each).await?;
237 if let Some(v) = res {
238 results.push(v);
239 }
240 }
241 Ok(results)
242 }
243 /// Get JSR-to-npm package meta
244 ///
245 /// The difference for [Meta] other than the compability,
246 /// is that you dont need to fetch again for specific version.
247 /// You just need to get from [NpmCompMeta::versions], and then the result will show up
248 ///
249 /// Returns `Ok(None)` if the server returned `404`.
250 /// If the other failed status code returned, `Err` will be returned.
251 /// Else, `Ok(Some(NpmCompMeta))` returned normally as success
252 ///
253 /// See https://jsr.io/docs/api#package-version-metadata
254 ///
255 /// ## Errors
256 ///
257 /// Throws [reqwest::Error] or [serde_json::Error]
258 pub async fn get_npm_comp_meta(&self, value: impl AsRef<MetaBuilder>) -> Result<Option<NpmCompMeta>> {
259 let res = value.as_ref();
260 return self
261 .fetcher::<NpmCompMeta>(HostSelector::NpmComp, format!("@{}/{}__{}", self.provider_scope, res.scope, res.name))
262 .await;
263 }
264 /// Get JSR-to-npm package metas
265 ///
266 /// The difference for [Meta] other than the compability,
267 /// is that you dont need to fetch again for specific version.
268 /// You just need to get [NpmCompMeta::versions] from one of them, and then the result will show up
269 ///
270 /// See https://jsr.io/docs/api#package-version-metadata
271 ///
272 /// ## Errors
273 ///
274 /// Throws [reqwest::Error] or [serde_json::Error]
275 pub async fn get_npm_comp_metas<T: AsRef<MetaBuilder>, U: IntoIterator<Item = T> + ExactSizeIterator>(&self, values: U) -> Result<Vec<NpmCompMeta>> {
276 let mut results = Vec::with_capacity(values.len());
277 for each in values {
278 let res = self.get_npm_comp_meta(each).await?;
279 if let Some(v) = res {
280 results.push(v);
281 }
282 }
283 Ok(results)
284 }
285}
286impl std::default::Default for Fetcher {
287 /// Creates a new fetcher with default configuration
288 ///
289 /// ## Panics
290 ///
291 /// See the docs for [reqwest::Client::new]. You can prevent this by making your own [FetcherBuilder] and set the [reqwest] [Client] to [Self::new]
292 fn default() -> Self {
293 return Self::new(FetcherBuilder::new())
294 }
295}