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 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 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}