Skip to main content

cognite/api/request_builder/
builder.rs

1use reqwest::header::{HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE, USER_AGENT};
2
3use prost::Message;
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6
7use crate::ApiClient;
8use crate::CondSend;
9use crate::CondSync;
10use crate::Error;
11use crate::SkipAuthentication;
12use reqwest::{IntoUrl, Response};
13
14use crate::Result;
15
16use super::{
17    JsonResponseHandler, NoResponseHandler, ProtoResponseHandler, RawResponseHandler,
18    ResponseHandler,
19};
20
21/// Generic request builder. Used to construct custom requests towards CDF.
22pub struct RequestBuilder<'a, T = ()> {
23    inner: reqwest_middleware::RequestBuilder,
24    client: &'a ApiClient,
25    output: T,
26}
27
28impl<'a> RequestBuilder<'a, ()> {
29    /// Create a POST request to `url`.
30    ///
31    /// # Arguments
32    ///
33    /// * `client` - API Client
34    /// * `url` - URL to send request to.
35    pub fn post(client: &'a ApiClient, url: impl IntoUrl) -> RequestBuilder<'a, ()> {
36        RequestBuilder {
37            inner: client.client().post(url),
38            client,
39            output: (),
40        }
41    }
42
43    /// Create a GET request to `url`.
44    ///
45    /// # Arguments
46    ///
47    /// * `client` - API Client
48    /// * `url` - URL to send request to.
49    pub fn get(client: &'a ApiClient, url: impl IntoUrl) -> RequestBuilder<'a, ()> {
50        RequestBuilder {
51            inner: client.client().get(url),
52            client,
53            output: (),
54        }
55    }
56
57    /// Create a DELETE request to `url`.
58    ///
59    /// # Arguments
60    ///
61    /// * `client` - API Client
62    /// * `url` - URL to send request to.
63    pub fn delete(client: &'a ApiClient, url: impl IntoUrl) -> RequestBuilder<'a, ()> {
64        RequestBuilder {
65            inner: client.client().delete(url),
66            client,
67            output: (),
68        }
69    }
70
71    /// Create a PUT request to `url`.
72    ///
73    /// # Arguments
74    ///
75    /// * `client` - API Client
76    /// * `url` - URL to send request to.
77    pub fn put(client: &'a ApiClient, url: impl IntoUrl) -> RequestBuilder<'a, ()> {
78        RequestBuilder {
79            inner: client.client().put(url),
80            client,
81            output: (),
82        }
83    }
84}
85
86impl<'a, T> RequestBuilder<'a, T> {
87    /// Add a header to the request.
88    ///
89    /// # Arguments
90    ///
91    /// * `key` - Header key
92    /// * `value` - Header value
93    pub fn header<K, V>(mut self, key: K, value: V) -> Self
94    where
95        HeaderName: TryFrom<K>,
96        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
97        HeaderValue: TryFrom<V>,
98        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
99    {
100        self.inner = self.inner.header(key, value);
101        self
102    }
103
104    /// Add a query to the request.
105    pub fn query<Q: Serialize + ?Sized>(mut self, query: &Q) -> Self {
106        self.inner = self.inner.query(query);
107        self
108    }
109
110    /// Add a JSON body to the request. This sets `CONTENT_TYPE`.
111    pub fn json<B: Serialize + ?Sized>(mut self, body: &B) -> Result<Self> {
112        self.inner = self.inner.header(
113            CONTENT_TYPE,
114            const { HeaderValue::from_static("application/json") },
115        );
116        Ok(self.body(serde_json::to_vec(body)?))
117    }
118
119    /// Add a body to the request. You will typically want to set `CONTENT_TYPE` yourself.
120    pub fn body(mut self, body: impl Into<reqwest::Body>) -> Self {
121        self.inner = self.inner.body(body);
122        self
123    }
124
125    /// Add a protobuf message as body to the request. This sets `CONTENT_TYPE`
126    pub fn protobuf<B: Message>(mut self, body: &B) -> Self {
127        self.inner = self.inner.header(
128            CONTENT_TYPE,
129            const { HeaderValue::from_static("application/protobuf") },
130        );
131        self.body(body.encode_to_vec())
132    }
133
134    /// Omit authentication headers in the request.
135    ///
136    /// This should be set before calling untrusted URLs.
137    pub fn omit_auth_headers(mut self) -> Self {
138        self.inner = self.inner.with_extension(SkipAuthentication);
139        self
140    }
141
142    /// Modify the inner request builder.
143    pub fn with_inner<
144        R: FnOnce(reqwest_middleware::RequestBuilder) -> reqwest_middleware::RequestBuilder,
145    >(
146        mut self,
147        m: R,
148    ) -> Self {
149        self.inner = m(self.inner);
150        self
151    }
152}
153
154impl<'a> RequestBuilder<'a, ()> {
155    /// Expect the response for a successful request to be `T` encoded as JSON.
156    pub fn accept_json<T: DeserializeOwned>(self) -> RequestBuilder<'a, JsonResponseHandler<T>> {
157        self.accept(JsonResponseHandler::new())
158    }
159
160    /// Expect the response for a successful request to be `T` encoded as protobuf.
161    pub fn accept_protobuf<T: Message + Default + CondSend + CondSync>(
162        self,
163    ) -> RequestBuilder<'a, ProtoResponseHandler<T>> {
164        self.accept(ProtoResponseHandler::new())
165    }
166
167    /// Ignore the response for a successful request.
168    pub fn accept_nothing(self) -> RequestBuilder<'a, NoResponseHandler> {
169        self.accept(NoResponseHandler)
170    }
171
172    /// Simply return the raw payload on a successful request.
173    pub fn accept_raw(self) -> RequestBuilder<'a, RawResponseHandler> {
174        self.accept(RawResponseHandler)
175    }
176
177    /// Set the response handler `T`.
178    pub fn accept<T: ResponseHandler>(self, handler: T) -> RequestBuilder<'a, T> {
179        RequestBuilder {
180            inner: self
181                .inner
182                .header(ACCEPT, const { HeaderValue::from_static(T::ACCEPT_HEADER) }),
183            client: self.client,
184            output: handler,
185        }
186    }
187}
188
189async fn handle_error(response: Response) -> Error {
190    let request_id = response
191        .headers()
192        .get("x-request-id")
193        .and_then(|x| x.to_str().ok())
194        .map(|x| x.to_string());
195
196    let status = response.status();
197
198    match &response.text().await {
199        Ok(s) => match serde_json::from_str(s) {
200            Ok(error_message) => Error::new_from_cdf(status, error_message, request_id),
201            Err(e) => Error::new_without_json(status, format!("{e}. Raw: {s}"), request_id),
202        },
203        Err(e) => Error::new_without_json(status, e.to_string(), request_id),
204    }
205}
206
207const SDK_USER_AGENT: &str = concat!("CogniteSdkRust/", env!("CARGO_PKG_VERSION"));
208const SDK_VERSION: &str = concat!("rust-sdk-v", env!("CARGO_PKG_VERSION"));
209
210impl<T: ResponseHandler> RequestBuilder<'_, T> {
211    /// Send the request. This sets a few core headers, and converts any errors into
212    /// [crate::Error]
213    pub async fn send(mut self) -> Result<T::Output> {
214        self.inner.extensions().insert(self.client.client().clone());
215
216        self.inner = self
217            .inner
218            .header(
219                USER_AGENT,
220                const { HeaderValue::from_static(SDK_USER_AGENT) },
221            )
222            .header("x-cdp-sdk", const { HeaderValue::from_static(SDK_VERSION) })
223            .header(
224                "x-cdp-app",
225                HeaderValue::from_str(self.client.app_name()).expect("Invalid app name"),
226            );
227        if let Some(cdf_version) = self.client.api_version() {
228            self.inner = self.inner.header(
229                "cdf-version",
230                HeaderValue::from_str(cdf_version).expect("Invalid CDF version"),
231            );
232        }
233
234        match self.inner.send().await {
235            Ok(response) => {
236                if response.status().is_success() {
237                    self.output.handle_response(response).await
238                } else {
239                    Err(handle_error(response).await)
240                }
241            }
242            Err(e) => Err(e.into()),
243        }
244    }
245}