dkp_core/registry/
client.rs1use 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}