jsonapis/
client.rs

1use super::*;
2
3use std::{fmt::Display, str::Utf8Error};
4
5use reqwest::{
6    blocking::{Client as ReqClient, RequestBuilder},
7    header::{HeaderValue, ACCEPT, CONTENT_TYPE, LOCATION},
8    Error as ReqError, StatusCode, Url,
9};
10use serde::Serialize;
11use serde_json::Error as JsonError;
12use url::ParseError;
13
14const MIME: &str = "application/vnd.api+json";
15const MIME_PREFIX: &str = "application/vnd.api+json;";
16
17#[derive(Clone, Debug)]
18pub struct Client {
19    url: String,
20    add_json_ext: bool,
21}
22
23pub type Result = std::result::Result<Response, Error>;
24
25#[derive(Clone, Debug, Serialize)]
26pub struct Response {
27    document: Document,
28    location: Option<String>,
29}
30
31#[derive(Debug)]
32pub enum Error {
33    Response(Box<Response>),
34    Url(ParseError),
35    Http(ReqError),
36    InvalidStatus(StatusCode),
37    NoContentType,
38    InvalidContentType(HeaderValue),
39    InvalidLocationUtf8(Utf8Error),
40    Text(ReqError),
41    Json(JsonError),
42}
43
44impl Response {
45    pub fn document(&self) -> &Document {
46        &self.document
47    }
48}
49
50impl Client {
51    pub fn new<U: Into<String>>(url: U) -> Self {
52        Self {
53            url: url.into(),
54            add_json_ext: false,
55        }
56    }
57
58    pub fn add_json_ext(self, add_json_ext: bool) -> Self {
59        Self {
60            add_json_ext,
61            ..self
62        }
63    }
64
65    pub fn get<P, I, K, V>(&self, path: P, params: I) -> Result
66    where
67        P: Display,
68        I: IntoIterator,
69        K: AsRef<str>,
70        V: AsRef<str>,
71        <I as IntoIterator>::Item: std::borrow::Borrow<(K, V)>,
72    {
73        let url = self.url_for_get(path, params).map_err(Error::Url)?;
74
75        let (status, response) = Self::make_request(ReqClient::new().get(url))?;
76
77        // TODO: Implement status handling accorging to specification
78        // https://jsonapi.org/format/#fetching-resources-responses
79        // https://jsonapi.org/format/#fetching-relationships-responses
80        if status.is_success() {
81            if status == StatusCode::OK {
82                Ok(response)
83            } else {
84                Err(Error::InvalidStatus(status))
85            }
86        } else {
87            Err(Error::Response(Box::new(response)))
88        }
89    }
90
91    pub fn post<'d, P, D>(&self, path: P, document: D) -> Result
92    where
93        P: Display,
94        D: Into<&'d Document>,
95    {
96        let url = self.url_for_post(path).map_err(Error::Url)?;
97
98        let document: &Document = document.into();
99
100        let (status, response) =
101            Self::make_request(ReqClient::new().post(url).json(document))?;
102
103        // TODO: Implement status handling accorging to specification
104        // https://jsonapi.org/format/#crud-creating-responses
105        // https://jsonapi.org/format/#crud-updating-responses
106        // https://jsonapi.org/format/#crud-updating-relationship-responses
107        // https://jsonapi.org/format/#crud-deleting-responses
108        if status.is_success() {
109            if status == StatusCode::CREATED {
110                Ok(response)
111            } else {
112                Err(Error::InvalidStatus(status))
113            }
114        } else {
115            Err(Error::Response(Box::new(response)))
116        }
117    }
118
119    fn url_for_get<P, I, K, V>(
120        &self,
121        path: P,
122        params: I,
123    ) -> std::result::Result<Url, ParseError>
124    where
125        P: Display,
126        I: IntoIterator,
127        K: AsRef<str>,
128        V: AsRef<str>,
129        <I as IntoIterator>::Item: std::borrow::Borrow<(K, V)>,
130    {
131        Url::parse_with_params(
132            &if self.add_json_ext {
133                format!("{}{}.json", self.url, path)
134            } else {
135                format!("{}{}", self.url, path)
136            },
137            params,
138        )
139    }
140
141    fn url_for_post<P>(&self, path: P) -> std::result::Result<Url, ParseError>
142    where
143        P: Display,
144    {
145        Url::parse(&if self.add_json_ext {
146            format!("{}{}.json", self.url, path)
147        } else {
148            format!("{}{}", self.url, path)
149        })
150    }
151
152    fn make_request(
153        request_builder: RequestBuilder,
154    ) -> std::result::Result<(StatusCode, Response), Error> {
155        let response = request_builder
156            .header(ACCEPT, MIME)
157            .header(CONTENT_TYPE, MIME)
158            .send()
159            .map_err(Error::Http)?;
160
161        let status = response.status();
162
163        let content_type = response
164            .headers()
165            .get(CONTENT_TYPE)
166            .ok_or(Error::NoContentType)?;
167
168        if content_type != MIME
169            && !content_type.as_bytes().starts_with(MIME_PREFIX.as_bytes())
170        {
171            return Err(Error::InvalidContentType(content_type.clone()));
172        }
173
174        let location = match response.headers().get(LOCATION) {
175            None => None,
176            Some(header) => match std::str::from_utf8(header.as_bytes()) {
177                Err(error) => return Err(Error::InvalidLocationUtf8(error)),
178                Ok(location) => Some(location.to_string()),
179            },
180        };
181
182        let json = response.text().map_err(Error::Text)?;
183
184        let document = serde_json::from_str(&json).map_err(Error::Json)?;
185
186        Ok((status, Response { document, location }))
187    }
188}