]> git.proxmox.com Git - proxmox.git/blob - proxmox-client/src/lib.rs
client: fixup checks for api calls not returning data
[proxmox.git] / proxmox-client / src / lib.rs
1 use std::collections::HashMap;
2 use std::future::Future;
3
4 use serde::{Deserialize, Serialize};
5 use serde_json::Value;
6
7 mod error;
8
9 pub use error::Error;
10
11 pub use proxmox_login::tfa::TfaChallenge;
12 pub use proxmox_login::{Authentication, Ticket};
13
14 pub(crate) mod auth;
15 pub use auth::{AuthenticationKind, Token};
16
17 #[cfg(feature = "hyper-client")]
18 mod client;
19 #[cfg(feature = "hyper-client")]
20 pub use client::{Client, TlsOptions};
21
22 /// HTTP client backend trait. This should be implemented for a HTTP client capable of making
23 /// *authenticated* API requests to a proxmox HTTP API.
24 pub trait HttpApiClient {
25 /// An API call should return a status code and the raw body.
26 type ResponseFuture<'a>: Future<Output = Result<HttpApiResponse, Error>> + 'a
27 where
28 Self: 'a;
29
30 /// `GET` request with a path and query component (no hostname).
31 ///
32 /// For this request, authentication headers should be set!
33 fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>;
34
35 /// `POST` request with a path and query component (no hostname), and a serializable body.
36 ///
37 /// The body should be serialized to json and sent with `Content-type: applicaion/json`.
38 ///
39 /// For this request, authentication headers should be set!
40 fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
41 where
42 T: ?Sized + Serialize;
43
44 /// `PUT` request with a path and query component (no hostname), and a serializable body.
45 ///
46 /// The body should be serialized to json and sent with `Content-type: applicaion/json`.
47 ///
48 /// For this request, authentication headers should be set!
49 fn put<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
50 where
51 T: ?Sized + Serialize;
52
53 /// `PUT` request with a path and query component (no hostname), no request body.
54 ///
55 /// For this request, authentication headers should be set!
56 fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>;
57
58 /// `DELETE` request with a path and query component (no hostname).
59 ///
60 /// For this request, authentication headers should be set!
61 fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>;
62 }
63
64 /// A response from the HTTP API as required by the [`HttpApiClient`] trait.
65 pub struct HttpApiResponse {
66 pub status: u16,
67 pub content_type: Option<String>,
68 pub body: Vec<u8>,
69 }
70
71 impl HttpApiResponse {
72 /// Expect a JSON response as returend by the `extjs` formatter.
73 pub fn expect_json<T>(self) -> Result<ApiResponseData<T>, Error>
74 where
75 T: for<'de> Deserialize<'de>,
76 {
77 self.assert_json_content_type()?;
78
79 serde_json::from_slice::<RawApiResponse<T>>(&self.body)
80 .map_err(|err| Error::bad_api("failed to parse api response", err))?
81 .check()
82 }
83
84 fn assert_json_content_type(&self) -> Result<(), Error> {
85 match self
86 .content_type
87 .as_deref()
88 .and_then(|v| v.split(';').next())
89 {
90 Some("application/json") => Ok(()),
91 Some(other) => Err(Error::BadApi(
92 format!("expected json body, got {other}",),
93 None,
94 )),
95 None => Err(Error::BadApi(
96 "expected json body, but no Content-Type was sent".to_string(),
97 None,
98 )),
99 }
100 }
101
102 /// Expect that the API call did *not* return any data in the `data` field.
103 pub fn nodata(self) -> Result<(), Error> {
104 let response = serde_json::from_slice::<RawApiResponse<()>>(&self.body)
105 .map_err(|err| Error::bad_api("failed to parse api response", err))?;
106
107 if response.data.is_some() {
108 Err(Error::UnexpectedData)
109 } else {
110 response.check_nodata()?;
111 Ok(())
112 }
113 }
114 }
115
116 /// API responses can have additional *attributes* added to their data.
117 pub struct ApiResponseData<T> {
118 pub attribs: HashMap<String, Value>,
119 pub data: T,
120 }
121
122 #[derive(serde::Deserialize)]
123 struct RawApiResponse<T> {
124 #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")]
125 status: Option<u16>,
126 message: Option<String>,
127 #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")]
128 success: Option<bool>,
129 data: Option<T>,
130
131 #[serde(default)]
132 errors: HashMap<String, String>,
133
134 #[serde(default, flatten)]
135 attribs: HashMap<String, Value>,
136 }
137
138 impl<T> RawApiResponse<T> {
139 fn check_success(mut self) -> Result<Self, Error> {
140 if self.success == Some(true) {
141 return Ok(self);
142 }
143
144 let status = http::StatusCode::from_u16(self.status.unwrap_or(400))
145 .unwrap_or(http::StatusCode::BAD_REQUEST);
146 let mut message = self
147 .message
148 .take()
149 .unwrap_or_else(|| "no message provided".to_string());
150 for (param, error) in self.errors {
151 use std::fmt::Write;
152 let _ = write!(message, "\n{param}: {error}");
153 }
154
155 Err(Error::api(status, message))
156 }
157
158 fn check(self) -> Result<ApiResponseData<T>, Error> {
159 let this = self.check_success()?;
160
161 Ok(ApiResponseData {
162 data: this
163 .data
164 .ok_or_else(|| Error::BadApi("api returned no data".to_string(), None))?,
165 attribs: this.attribs,
166 })
167 }
168
169 fn check_nodata(self) -> Result<ApiResponseData<()>, Error> {
170 let this = self.check_success()?;
171
172 Ok(ApiResponseData {
173 data: (),
174 attribs: this.attribs,
175 })
176 }
177 }
178
179 impl<'c, C> HttpApiClient for &'c C
180 where
181 C: HttpApiClient,
182 {
183 type ResponseFuture<'a> = C::ResponseFuture<'a>
184 where
185 Self: 'a;
186
187 fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
188 C::get(self, path_and_query)
189 }
190
191 fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
192 where
193 T: ?Sized + Serialize,
194 {
195 C::post(self, path_and_query, params)
196 }
197
198 fn put<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
199 where
200 T: ?Sized + Serialize,
201 {
202 C::put(self, path_and_query, params)
203 }
204
205 fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
206 C::put_without_body(self, path_and_query)
207 }
208
209 fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
210 C::delete(self, path_and_query)
211 }
212 }
213
214 impl<C> HttpApiClient for std::sync::Arc<C>
215 where
216 C: HttpApiClient,
217 {
218 type ResponseFuture<'a> = C::ResponseFuture<'a>
219 where
220 Self: 'a;
221
222 fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
223 C::get(self, path_and_query)
224 }
225
226 fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
227 where
228 T: ?Sized + Serialize,
229 {
230 C::post(self, path_and_query, params)
231 }
232
233 fn put<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
234 where
235 T: ?Sized + Serialize,
236 {
237 C::put(self, path_and_query, params)
238 }
239
240 fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
241 C::put_without_body(self, path_and_query)
242 }
243
244 fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
245 C::delete(self, path_and_query)
246 }
247 }
248
249 impl<C> HttpApiClient for std::rc::Rc<C>
250 where
251 C: HttpApiClient,
252 {
253 type ResponseFuture<'a> = C::ResponseFuture<'a>
254 where
255 Self: 'a;
256
257 fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
258 C::get(self, path_and_query)
259 }
260
261 fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
262 where
263 T: ?Sized + Serialize,
264 {
265 C::post(self, path_and_query, params)
266 }
267
268 fn put<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
269 where
270 T: ?Sized + Serialize,
271 {
272 C::put(self, path_and_query, params)
273 }
274
275 fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
276 C::put_without_body(self, path_and_query)
277 }
278
279 fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
280 C::delete(self, path_and_query)
281 }
282 }