Skip to main content

dkp_core/registry/
client.rs

1use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
2
3use crate::error::{DkpError, DkpResult};
4
5use super::types::{
6    PackVersionResponse, PublishRequest, PublishResponse, SearchResponse, VersionListResponse,
7};
8
9pub struct RegistryClient {
10    base_url: String,
11    token: Option<String>,
12    http: reqwest::Client,
13}
14
15impl RegistryClient {
16    pub fn new(base_url: impl Into<String>, token: Option<String>) -> Self {
17        Self {
18            base_url: base_url.into().trim_end_matches('/').to_owned(),
19            token,
20            http: reqwest::Client::builder()
21                .user_agent(concat!("dkp/", env!("CARGO_PKG_VERSION")))
22                .build()
23                .expect("failed to build HTTP client"),
24        }
25    }
26
27    fn url(&self, path: &str) -> String {
28        format!("{}/api/v1{}", self.base_url, path)
29    }
30
31    fn auth_header(&self) -> Option<String> {
32        self.token.as_ref().map(|t| format!("Bearer {t}"))
33    }
34
35    pub async fn resolve(&self, name: &str, version: &str) -> DkpResult<PackVersionResponse> {
36        let encoded = urlencoding::encode(name);
37        let url = self.url(&format!("/packages/{encoded}/{version}"));
38        let mut req = self.http.get(&url);
39        if let Some(auth) = self.auth_header() {
40            req = req.header(AUTHORIZATION, auth);
41        }
42        let resp = req
43            .send()
44            .await
45            .map_err(|e| DkpError::Registry(e.to_string()))?;
46        if resp.status() == 401 {
47            return Err(DkpError::Registry(
48                "authentication required for this pack".into(),
49            ));
50        }
51        if resp.status() == 403 {
52            return Err(DkpError::Registry(
53                "not authorized to access this pack".into(),
54            ));
55        }
56        if resp.status() == 404 {
57            return Err(DkpError::Registry(format!(
58                "pack {name}@{version} not found"
59            )));
60        }
61        if !resp.status().is_success() {
62            let status = resp.status();
63            let body = resp.text().await.unwrap_or_default();
64            return Err(DkpError::Registry(format!(
65                "registry error {status}: {body}"
66            )));
67        }
68        resp.json()
69            .await
70            .map_err(|e| DkpError::Registry(e.to_string()))
71    }
72
73    pub async fn list_versions(&self, name: &str) -> DkpResult<VersionListResponse> {
74        let encoded = urlencoding::encode(name);
75        let url = self.url(&format!("/packages/{encoded}/versions"));
76        let mut req = self.http.get(&url);
77        if let Some(auth) = self.auth_header() {
78            req = req.header(AUTHORIZATION, auth);
79        }
80        let resp = req
81            .send()
82            .await
83            .map_err(|e| DkpError::Registry(e.to_string()))?;
84        if !resp.status().is_success() {
85            let status = resp.status();
86            let body = resp.text().await.unwrap_or_default();
87            return Err(DkpError::Registry(format!(
88                "registry error {status}: {body}"
89            )));
90        }
91        resp.json()
92            .await
93            .map_err(|e| DkpError::Registry(e.to_string()))
94    }
95
96    pub async fn publish(&self, req: PublishRequest) -> DkpResult<PublishResponse> {
97        let url = self.url("/publish");
98        let auth = self
99            .auth_header()
100            .ok_or_else(|| DkpError::Registry("registry token required for publish".into()))?;
101        let resp = self
102            .http
103            .post(&url)
104            .header(AUTHORIZATION, auth)
105            .header(CONTENT_TYPE, "application/json")
106            .json(&req)
107            .send()
108            .await
109            .map_err(|e| DkpError::Registry(e.to_string()))?;
110        if !resp.status().is_success() {
111            let status = resp.status();
112            let body = resp.text().await.unwrap_or_default();
113            return Err(DkpError::Registry(format!(
114                "publish failed {status}: {body}"
115            )));
116        }
117        resp.json()
118            .await
119            .map_err(|e| DkpError::Registry(e.to_string()))
120    }
121
122    pub async fn yank(&self, name: &str, version: &str, reason: &str) -> DkpResult<()> {
123        let url = self.url("/yank");
124        let auth = self
125            .auth_header()
126            .ok_or_else(|| DkpError::Registry("registry token required for yank".into()))?;
127        let body = serde_json::json!({ "name": name, "version": version, "reason": reason });
128        let resp = self
129            .http
130            .post(&url)
131            .header(AUTHORIZATION, auth)
132            .header(CONTENT_TYPE, "application/json")
133            .json(&body)
134            .send()
135            .await
136            .map_err(|e| DkpError::Registry(e.to_string()))?;
137        if !resp.status().is_success() {
138            let status = resp.status();
139            let body = resp.text().await.unwrap_or_default();
140            return Err(DkpError::Registry(format!("yank failed {status}: {body}")));
141        }
142        Ok(())
143    }
144
145    pub async fn search(
146        &self,
147        q: &str,
148        domain: Option<&str>,
149        conformance: Option<&str>,
150        limit: u32,
151        offset: u32,
152    ) -> DkpResult<SearchResponse> {
153        let mut url = reqwest::Url::parse(&self.url("/search"))
154            .map_err(|e| DkpError::Registry(e.to_string()))?;
155        url.query_pairs_mut()
156            .append_pair("q", q)
157            .append_pair("limit", &limit.to_string())
158            .append_pair("offset", &offset.to_string());
159        if let Some(d) = domain {
160            url.query_pairs_mut().append_pair("domain", d);
161        }
162        if let Some(c) = conformance {
163            url.query_pairs_mut().append_pair("conformance", c);
164        }
165        let mut req = self.http.get(url);
166        if let Some(auth) = self.auth_header() {
167            req = req.header(AUTHORIZATION, auth);
168        }
169        let resp = req
170            .send()
171            .await
172            .map_err(|e| DkpError::Registry(e.to_string()))?;
173        if !resp.status().is_success() {
174            let status = resp.status();
175            let body = resp.text().await.unwrap_or_default();
176            return Err(DkpError::Registry(format!(
177                "search failed {status}: {body}"
178            )));
179        }
180        resp.json()
181            .await
182            .map_err(|e| DkpError::Registry(e.to_string()))
183    }
184}