cognite/api/api_client.rs
1use crate::{CondSend, IntoParams};
2use anyhow::anyhow;
3use bytes::Bytes;
4use futures::{TryStream, TryStreamExt};
5use prost::Message;
6use reqwest::header::{HeaderValue, CONTENT_TYPE};
7use reqwest::Response;
8use reqwest_middleware::ClientWithMiddleware;
9use serde::de::DeserializeOwned;
10use serde::ser::Serialize;
11
12use crate::error::{Error, Result};
13
14use super::request_builder::RequestBuilder;
15
16/// API client, used to query CDF.
17pub struct ApiClient {
18 api_base_url: String,
19 app_name: String,
20 client: ClientWithMiddleware,
21 api_version: Option<String>,
22}
23
24impl ApiClient {
25 /// Create a new api client.
26 ///
27 /// # Arguments
28 ///
29 /// * `api_base_url` - Base URL for CDF. For example `https://api.cognitedata.com`
30 /// * `app_name` - App name added to the `x-cdp-app` header.
31 /// * `client` - Underlying reqwest client.
32 pub fn new(api_base_url: &str, app_name: &str, client: ClientWithMiddleware) -> ApiClient {
33 ApiClient {
34 api_base_url: String::from(api_base_url),
35 app_name: String::from(app_name),
36 client,
37 api_version: None,
38 }
39 }
40
41 /// Create a new api client with a custom API version.
42 /// This will set the `cdf-version` header to the given value.
43 ///
44 /// # Arguments
45 ///
46 /// * `api_version` - API version to use.
47 pub fn clone_with_api_version(&self, api_version: &str) -> ApiClient {
48 // We do clone the base URL here, but note that the client is internally
49 // cloneable, so we do still share it cross resources, which is important.
50 ApiClient {
51 api_base_url: self.api_base_url.clone(),
52 app_name: self.app_name.clone(),
53 client: self.client.clone(),
54 api_version: Some(api_version.to_string()),
55 }
56 }
57
58 /// Perform a get request to the given path, deserializing the result from JSON.
59 ///
60 /// # Arguments
61 ///
62 /// * `path` - Request path, without leading slash.
63 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
64 RequestBuilder::get(self, format!("{}/{}", self.api_base_url, path))
65 .accept_json()
66 .send()
67 .await
68 }
69
70 /// Perform a get request to the given path, with a query given by `params`,
71 /// then deserialize the result from JSON.
72 ///
73 /// # Arguments
74 ///
75 /// * `path` - Request path, without leading slash.
76 /// * `params` - Optional object converted to query parameters.
77 pub async fn get_with_params<T: DeserializeOwned, R: IntoParams>(
78 &self,
79 path: &str,
80 params: Option<R>,
81 ) -> Result<T> {
82 let mut b =
83 RequestBuilder::get(self, format!("{}/{}", self.api_base_url, path)).accept_json();
84
85 if let Some(params) = params {
86 b = b.query(¶ms.into_params());
87 }
88
89 b.send().await
90 }
91
92 /// Perform a get request to the given URL, returning a stream.
93 ///
94 /// # Arguments
95 ///
96 /// * `url` - Full URL to get stream from.
97 pub async fn get_stream(
98 &self,
99 url: &str,
100 ) -> Result<impl TryStream<Ok = bytes::Bytes, Error = reqwest::Error>> {
101 let r = RequestBuilder::get(self, url)
102 .omit_auth_headers()
103 .accept_raw()
104 .send()
105 .await?;
106 Ok(r.bytes_stream())
107 }
108
109 /// Perform a post request to the given path, serializing `object` to JSON and sending it
110 /// as the body, then deserialize the response from JSON.
111 ///
112 /// # Arguments
113 ///
114 /// * `path` - Request path, without leading slash.
115 /// * `object` - Object converted to JSON body.
116 pub async fn post<D, S>(&self, path: &str, object: &S) -> Result<D>
117 where
118 D: DeserializeOwned,
119 S: Serialize,
120 {
121 RequestBuilder::post(self, format!("{}/{}", self.api_base_url, path))
122 .json(object)?
123 .accept_json()
124 .send()
125 .await
126 }
127
128 /// Create a request builder for a `GET` request to `path`.
129 pub fn get_request(&self, path: &str) -> RequestBuilder<'_, ()> {
130 RequestBuilder::get(self, format!("{}/{}", self.api_base_url, path))
131 }
132
133 /// Create a request builder for a `POST` request to `path`.
134 pub fn post_request(&self, path: &str) -> RequestBuilder<'_, ()> {
135 RequestBuilder::post(self, format!("{}/{}", self.api_base_url, path))
136 }
137
138 /// Create a request builder for a `PUT` request to `path`.
139 pub fn put_request(&self, path: &str) -> RequestBuilder<'_, ()> {
140 RequestBuilder::put(self, format!("{}/{}", self.api_base_url, path))
141 }
142
143 /// Create a request builder for a `Delete` request to `path`.
144 pub fn delete_request(&self, path: &str) -> RequestBuilder<'_, ()> {
145 RequestBuilder::delete(self, format!("{}/{}", self.api_base_url, path))
146 }
147
148 /// Perform a post request to the given path, with query parameters given by `params`.
149 /// Deserialize the response from JSON.
150 ///
151 /// * `path` - Request path, without leading slash.
152 /// * `object` - Object converted to JSON body.
153 /// * `params` - Object converted to query parameters.
154 pub async fn post_with_query<D: DeserializeOwned, S: Serialize, R: IntoParams>(
155 &self,
156 path: &str,
157 object: &S,
158 params: Option<R>,
159 ) -> Result<D> {
160 let mut b = self.post_request(path).json(object)?;
161 if let Some(params) = params {
162 b = b.query(¶ms.into_params());
163 }
164 b.accept_json().send().await
165 }
166
167 /// Perform a post request to the given path, posting `value` as protobuf.
168 /// Expects JSON as response.
169 ///
170 /// # Arguments
171 ///
172 /// * `path` - Request path without leading slash.
173 /// * `value` - Protobuf value to post.
174 pub async fn post_protobuf<D: DeserializeOwned + Send + Sync, T: Message>(
175 &self,
176 path: &str,
177 value: &T,
178 ) -> Result<D> {
179 self.post_request(path)
180 .protobuf(value)
181 .accept_json()
182 .send()
183 .await
184 }
185
186 /// Perform a post request to the given path, send `object` as JSON in the body,
187 /// then expect protobuf as response.
188 ///
189 /// # Arguments
190 ///
191 /// * `path` - Request path without leading slash.
192 /// * `object` - Object to convert to JSON and post.
193 pub async fn post_expect_protobuf<D: Message + Default, S: Serialize>(
194 &self,
195 path: &str,
196 object: &S,
197 ) -> Result<D> {
198 self.post_request(path)
199 .json(object)?
200 .accept_protobuf()
201 .send()
202 .await
203 }
204
205 /// Perform a put request with the data in `data`.
206 ///
207 /// # Arguments
208 ///
209 /// * `url` - URL to stream blob to.
210 /// * `mime_type` - What to put in the `X-Upload_Content-Type` header.
211 /// * `data` - Data to upload.
212 pub async fn put_blob(&self, url: &str, mime_type: &str, data: impl Into<Bytes>) -> Result<()> {
213 let bytes: Bytes = data.into();
214 let mut b = RequestBuilder::put(self, url)
215 .body(bytes)
216 .omit_auth_headers()
217 .accept_nothing();
218 if !mime_type.is_empty() {
219 b = b
220 .header(CONTENT_TYPE, HeaderValue::from_str(mime_type)?)
221 .header("X-Upload-Content-Type", HeaderValue::from_str(mime_type)?);
222 }
223 b.send().await
224 }
225
226 /// Perform a put request, streaming data to `url`.
227 ///
228 /// # Arguments
229 ///
230 /// * `url` - URL to stream blob to.
231 /// * `mime_type` - What to put in the `X-Upload_Content-Type` header.
232 /// * `stream` - Stream to upload.
233 /// * `stream_chunked` - If `true`, use chunked streaming to upload the data.
234 /// * `known_size` - Set the `Content-Length` header to this value.
235 ///
236 /// If `known_size` is `None` and `stream_chunked` is `true`, the request will be uploaded using
237 /// special chunked streaming logic. Some backends do not support this.
238 ///
239 /// If `stream_chunked` is true and `known_size` is Some, this will include a content length header,
240 /// it is highly recommended to set this whenever possible.
241 ///
242 /// # Warning
243 /// If `stream_chunked` is false, this will collect the input stream into a memory, which can
244 /// be _very_ expensive.
245 ///
246 /// # Note on WASM
247 ///
248 /// Note that wasm32-unknown-unknown targets do not support chunked streaming uploads,
249 /// so effectively `stream_chunked` is always false.
250 #[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
251 pub async fn put_stream<S>(
252 &self,
253 url: &str,
254 mime_type: &str,
255 stream: S,
256 stream_chunked: bool,
257 known_size: Option<u64>,
258 ) -> Result<()>
259 where
260 S: futures::TryStream + CondSend + 'static,
261 S::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
262 bytes::Bytes: From<S::Ok>,
263 {
264 let mut b = RequestBuilder::put(self, url)
265 .omit_auth_headers()
266 .accept_nothing();
267 if !mime_type.is_empty() {
268 b = b
269 .header(CONTENT_TYPE, HeaderValue::from_str(mime_type)?)
270 .header("X-Upload-Content-Type", HeaderValue::from_str(mime_type)?);
271 }
272
273 #[cfg(not(target_arch = "wasm32"))]
274 if stream_chunked {
275 if let Some(size) = known_size {
276 b = b.header(
277 reqwest::header::CONTENT_LENGTH,
278 HeaderValue::from_str(&size.to_string())?,
279 );
280 }
281 b = b.body(reqwest::Body::wrap_stream(stream));
282 return b.send().await;
283 }
284
285 let body: Vec<S::Ok> = stream
286 .try_collect()
287 .await
288 .map_err(|e| Error::StreamError(anyhow!(e.into())))?;
289 let body: Vec<u8> = body
290 .into_iter()
291 .flat_map(Into::<bytes::Bytes>::into)
292 .collect();
293 b = b.body(body);
294 b.send().await
295 }
296
297 /// Perform a put request to `path` with `object` as JSON, expecting JSON in return.
298 ///
299 /// # Arguments
300 ///
301 /// * `path` - Request path without leading slash.
302 /// * `object` - Object to send as JSON.
303 pub async fn put<D, S>(&self, path: &str, object: &S) -> Result<D>
304 where
305 D: DeserializeOwned,
306 S: Serialize,
307 {
308 self.put_request(path)
309 .json(object)?
310 .accept_json()
311 .send()
312 .await
313 }
314
315 /// Perform a delete request to `path`, expecting JSON as response.
316 ///
317 /// # Arguments
318 ///
319 /// * `path` - Request path without leading slash.
320 pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
321 self.delete_request(path).accept_json().send().await
322 }
323
324 /// Perform a delete request to `path`, with query parameters given by `params`.
325 ///
326 /// # Arguments
327 ///
328 /// * `path` - Request path without leading slash.
329 /// * `params` - Object converted to query parameters.
330 pub async fn delete_with_params<T: DeserializeOwned, R: IntoParams>(
331 &self,
332 path: &str,
333 params: Option<R>,
334 ) -> Result<T> {
335 let mut b = self.delete_request(path).accept_json();
336 if let Some(params) = params {
337 b = b.query(¶ms.into_params());
338 }
339 b.send().await
340 }
341
342 /// Send an arbitrary HTTP request using the client. This will not parse the response,
343 /// but will append authentication headers and retry with the same semantics as any
344 /// normal API call.
345 ///
346 /// # Arguments
347 ///
348 /// * `request_builder` - Request to send.
349 pub async fn send_request(
350 &self,
351 mut request_builder: reqwest_middleware::RequestBuilder,
352 ) -> Result<Response> {
353 request_builder.extensions().insert(self.client.clone());
354
355 Ok(request_builder.send().await?)
356 }
357
358 /// Get the inner HTTP client.
359 pub fn client(&self) -> &ClientWithMiddleware {
360 &self.client
361 }
362
363 /// Get the configured app name.
364 pub fn app_name(&self) -> &str {
365 &self.app_name
366 }
367
368 /// Get the configured API base URL.
369 pub fn api_base_url(&self) -> &str {
370 &self.api_base_url
371 }
372
373 /// Get the CDF API version this client will use.
374 /// Any requests made with the request builder will append a header
375 /// cdf-version with this value, if it is set.
376 pub fn api_version(&self) -> Option<&str> {
377 self.api_version.as_deref()
378 }
379}