binstalk_registry/
sparse_registry.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use std::fmt::Display;

use binstalk_downloader::remote::{Client, Error as RemoteError};
use binstalk_types::cargo_toml_binstall::Meta;
use cargo_toml_workspace::cargo_toml::Manifest;
use compact_str::CompactString;
use semver::VersionReq;
use serde_json::Deserializer as JsonDeserializer;
use tokio::sync::OnceCell;
use tracing::instrument;
use url::Url;

use crate::{
    crate_prefix_components, parse_manifest, render_dl_template, MatchedVersion, RegistryConfig,
    RegistryError,
};

#[derive(Debug)]
pub struct SparseRegistry {
    url: Url,
    dl_template: OnceCell<CompactString>,
}

impl SparseRegistry {
    /// * `url` - `url.cannot_be_a_base()` must be `false`
    pub fn new(url: Url) -> Self {
        Self {
            url,
            dl_template: Default::default(),
        }
    }

    pub fn url(&self) -> impl Display + '_ {
        &self.url
    }

    async fn get_dl_template(&self, client: &Client) -> Result<&str, RegistryError> {
        self.dl_template
            .get_or_try_init(|| {
                Box::pin(async {
                    let mut url = self.url.clone();
                    url.path_segments_mut().unwrap().push("config.json");
                    let config: RegistryConfig = client.get(url).send(true).await?.json().await?;
                    Ok(config.dl)
                })
            })
            .await
            .map(AsRef::as_ref)
    }

    /// `url` must be a valid http(s) url.
    async fn find_crate_matched_ver(
        client: &Client,
        mut url: Url,
        crate_name: &str,
        (c1, c2): &(CompactString, Option<CompactString>),
        version_req: &VersionReq,
    ) -> Result<MatchedVersion, RegistryError> {
        {
            let mut path = url.path_segments_mut().unwrap();

            path.push(c1);
            if let Some(c2) = c2 {
                path.push(c2);
            }

            path.push(&crate_name.to_lowercase());
        }

        let body = client
            .get(url)
            .send(true)
            .await
            .map_err(|e| match e {
                RemoteError::Http(e) if e.is_status() => RegistryError::NotFound(crate_name.into()),
                e => e.into(),
            })?
            .bytes()
            .await
            .map_err(RegistryError::from)?;
        MatchedVersion::find(
            &mut JsonDeserializer::from_slice(&body).into_iter(),
            version_req,
        )
    }

    #[instrument(
        skip(self, client, version_req),
        fields(
            registry_url = format_args!("{}", self.url),
            version_req = format_args!("{version_req}"),
        )
    )]
    pub async fn fetch_crate_matched(
        &self,
        client: Client,
        crate_name: &str,
        version_req: &VersionReq,
    ) -> Result<Manifest<Meta>, RegistryError> {
        let crate_prefix = crate_prefix_components(crate_name)?;
        let dl_template = self.get_dl_template(&client).await?;
        let matched_version = Self::find_crate_matched_ver(
            &client,
            self.url.clone(),
            crate_name,
            &crate_prefix,
            version_req,
        )
        .await?;
        let dl_url = Url::parse(&render_dl_template(
            dl_template,
            crate_name,
            &crate_prefix,
            &matched_version,
        )?)?;

        parse_manifest(client, crate_name, dl_url, matched_version).await
    }
}