gitlab/api/
raw.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use async_trait::async_trait;
8use http::{header, Request};
9
10use crate::api::{query, ApiError, AsyncClient, AsyncQuery, Client, Endpoint, Query};
11
12/// A query modifier that returns the raw data from the endpoint.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Raw<E> {
15    endpoint: E,
16}
17
18/// Return the raw data from the endpoint.
19pub fn raw<E>(endpoint: E) -> Raw<E> {
20    Raw {
21        endpoint,
22    }
23}
24
25impl<E, C> Query<Vec<u8>, C> for Raw<E>
26where
27    E: Endpoint,
28    C: Client,
29{
30    fn query(&self, client: &C) -> Result<Vec<u8>, ApiError<C::Error>> {
31        let mut url = self
32            .endpoint
33            .url_base()
34            .endpoint_for(client, &self.endpoint.endpoint())?;
35        self.endpoint.parameters().add_to_url(&mut url);
36
37        let req = Request::builder()
38            .method(self.endpoint.method())
39            .uri(query::url_to_http_uri(url));
40        let (req, data) = if let Some((mime, data)) = self.endpoint.body()? {
41            let req = req.header(header::CONTENT_TYPE, mime);
42            (req, data)
43        } else {
44            (req, Vec::new())
45        };
46        let rsp = client.rest(req, data)?;
47        let status = rsp.status();
48        if !status.is_success() {
49            let v = if let Ok(v) = serde_json::from_slice(rsp.body()) {
50                v
51            } else {
52                return Err(ApiError::server_error(status, rsp.body()));
53            };
54            return Err(ApiError::from_gitlab_with_status(status, v));
55        } else if status == http::StatusCode::MOVED_PERMANENTLY {
56            return Err(ApiError::moved_permanently(
57                rsp.headers().get(http::header::LOCATION),
58            ));
59        }
60
61        Ok(rsp.into_body().as_ref().into())
62    }
63}
64
65#[async_trait]
66impl<E, C> AsyncQuery<Vec<u8>, C> for Raw<E>
67where
68    E: Endpoint + Sync,
69    C: AsyncClient + Sync,
70{
71    async fn query_async(&self, client: &C) -> Result<Vec<u8>, ApiError<C::Error>> {
72        let mut url = self
73            .endpoint
74            .url_base()
75            .endpoint_for(client, &self.endpoint.endpoint())?;
76        self.endpoint.parameters().add_to_url(&mut url);
77
78        let req = Request::builder()
79            .method(self.endpoint.method())
80            .uri(query::url_to_http_uri(url));
81        let (req, data) = if let Some((mime, data)) = self.endpoint.body()? {
82            let req = req.header(header::CONTENT_TYPE, mime);
83            (req, data)
84        } else {
85            (req, Vec::new())
86        };
87        let rsp = client.rest_async(req, data).await?;
88        let status = rsp.status();
89        if !status.is_success() {
90            let v = if let Ok(v) = serde_json::from_slice(rsp.body()) {
91                v
92            } else {
93                return Err(ApiError::server_error(status, rsp.body()));
94            };
95            return Err(ApiError::from_gitlab_with_status(status, v));
96        } else if status == http::StatusCode::MOVED_PERMANENTLY {
97            return Err(ApiError::moved_permanently(
98                rsp.headers().get(http::header::LOCATION),
99            ));
100        }
101
102        Ok(rsp.into_body().as_ref().into())
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use http::StatusCode;
109    use serde_json::json;
110
111    use crate::api::endpoint_prelude::*;
112    use crate::api::{self, ApiError, AsyncQuery, Query};
113    use crate::test::client::{ExpectedUrl, SingleTestClient};
114
115    struct Dummy;
116
117    impl Endpoint for Dummy {
118        fn method(&self) -> Method {
119            Method::GET
120        }
121
122        fn endpoint(&self) -> Cow<'static, str> {
123            "dummy".into()
124        }
125    }
126
127    #[test]
128    fn test_gitlab_non_json_response() {
129        let endpoint = ExpectedUrl::builder().endpoint("dummy").build().unwrap();
130        let client = SingleTestClient::new_raw(endpoint, "not json");
131
132        let data = api::raw(Dummy).query(&client).unwrap();
133        itertools::assert_equal(data, "not json".bytes());
134    }
135
136    #[tokio::test]
137    async fn test_gitlab_non_json_response_async() {
138        let endpoint = ExpectedUrl::builder().endpoint("dummy").build().unwrap();
139        let client = SingleTestClient::new_raw(endpoint, "not json");
140
141        let data = api::raw(Dummy).query_async(&client).await.unwrap();
142        itertools::assert_equal(data, "not json".bytes());
143    }
144
145    #[test]
146    fn test_gitlab_error_bad_json() {
147        let endpoint = ExpectedUrl::builder()
148            .endpoint("dummy")
149            .status(StatusCode::NOT_FOUND)
150            .build()
151            .unwrap();
152        let client = SingleTestClient::new_raw(endpoint, "");
153
154        let err = api::raw(Dummy).query(&client).unwrap_err();
155        if let ApiError::GitlabService {
156            status, ..
157        } = err
158        {
159            assert_eq!(status, StatusCode::NOT_FOUND);
160        } else {
161            panic!("unexpected error: {}", err);
162        }
163    }
164
165    #[test]
166    fn test_gitlab_error_detection() {
167        let endpoint = ExpectedUrl::builder()
168            .endpoint("dummy")
169            .status(StatusCode::NOT_FOUND)
170            .build()
171            .unwrap();
172        let client = SingleTestClient::new_json(
173            endpoint,
174            &json!({
175                "message": "dummy error message",
176            }),
177        );
178
179        let err = api::raw(Dummy).query(&client).unwrap_err();
180        if let ApiError::GitlabWithStatus {
181            status,
182            msg,
183        } = err
184        {
185            assert_eq!(status, StatusCode::NOT_FOUND);
186            assert_eq!(msg, "dummy error message");
187        } else {
188            panic!("unexpected error: {}", err);
189        }
190    }
191
192    #[test]
193    fn test_gitlab_error_detection_legacy() {
194        let endpoint = ExpectedUrl::builder()
195            .endpoint("dummy")
196            .status(StatusCode::NOT_FOUND)
197            .build()
198            .unwrap();
199        let client = SingleTestClient::new_json(
200            endpoint,
201            &json!({
202                "error": "dummy error message",
203            }),
204        );
205
206        let err = api::raw(Dummy).query(&client).unwrap_err();
207        if let ApiError::GitlabWithStatus {
208            status,
209            msg,
210        } = err
211        {
212            assert_eq!(status, StatusCode::NOT_FOUND);
213            assert_eq!(msg, "dummy error message");
214        } else {
215            panic!("unexpected error: {}", err);
216        }
217    }
218
219    #[test]
220    fn test_gitlab_error_detection_unknown() {
221        let endpoint = ExpectedUrl::builder()
222            .endpoint("dummy")
223            .status(StatusCode::NOT_FOUND)
224            .build()
225            .unwrap();
226        let err_obj = json!({
227            "bogus": "dummy error message",
228        });
229        let client = SingleTestClient::new_json(endpoint, &err_obj);
230
231        let err = api::raw(Dummy).query(&client).unwrap_err();
232        if let ApiError::GitlabUnrecognizedWithStatus {
233            status,
234            obj,
235        } = err
236        {
237            assert_eq!(status, StatusCode::NOT_FOUND);
238            assert_eq!(obj, err_obj);
239        } else {
240            panic!("unexpected error: {}", err);
241        }
242    }
243}