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}