Skip to main content

iri_client/
blocking_client.rs

1use reqwest::{Method, Url};
2use serde_json::Value;
3
4use crate::ClientError;
5
6/// Generic blocking JSON REST client.
7///
8/// This is the synchronous counterpart of [`crate::ApiClient`].
9#[derive(Debug)]
10pub struct BlockingApiClient {
11    base_url: Url,
12    authorization_token: Option<String>,
13    http: reqwest::blocking::Client,
14}
15
16impl BlockingApiClient {
17    /// Creates a new client with the given base URL.
18    ///
19    /// The URL is normalized to include a trailing slash, so relative endpoint
20    /// paths join correctly.
21    pub fn new(base_url: impl AsRef<str>) -> Result<Self, ClientError> {
22        let parsed = Url::parse(base_url.as_ref())
23            .map_err(|_| ClientError::InvalidBaseUrl(base_url.as_ref().to_owned()))?;
24
25        Ok(Self {
26            base_url: ensure_trailing_slash(parsed),
27            authorization_token: None,
28            http: reqwest::blocking::Client::new(),
29        })
30    }
31
32    /// Returns a new client with a raw access token attached to all requests.
33    ///
34    /// This sets `Authorization: <token>` (without `Bearer ` prefix).
35    #[must_use]
36    pub fn with_authorization_token(mut self, token: impl Into<String>) -> Self {
37        self.authorization_token = Some(token.into());
38        self
39    }
40
41    /// Sends a `GET` request and parses the response as JSON.
42    pub fn get_json(&self, path: &str) -> Result<Value, ClientError> {
43        self.request_json(Method::GET, path, None)
44    }
45
46    /// Sends a `GET` request with query parameters and parses the response as JSON.
47    pub fn get_json_with_query(
48        &self,
49        path: &str,
50        query: &[(&str, &str)],
51    ) -> Result<Value, ClientError> {
52        self.request_json_with_query(Method::GET, path, query, None)
53    }
54
55    /// Sends a request and parses the response as JSON.
56    ///
57    /// Use [`Self::request_json_with_query`] when query parameters are needed.
58    pub fn request_json(
59        &self,
60        method: Method,
61        path: &str,
62        body: Option<Value>,
63    ) -> Result<Value, ClientError> {
64        self.request_json_with_query(method, path, &[], body)
65    }
66
67    /// Sends a request with query parameters and parses the response as JSON.
68    ///
69    /// Returns [`Value::Null`] for successful responses with an empty body.
70    pub fn request_json_with_query(
71        &self,
72        method: Method,
73        path: &str,
74        query: &[(&str, &str)],
75        body: Option<Value>,
76    ) -> Result<Value, ClientError> {
77        let url = self.build_url(path)?;
78        let mut request = self
79            .http
80            .request(method, url)
81            .header(reqwest::header::ACCEPT, "application/json");
82
83        if !query.is_empty() {
84            request = request.query(query);
85        }
86
87        if let Some(token) = &self.authorization_token {
88            request = request.bearer_auth(token);
89        }
90
91        if let Some(json_body) = body {
92            request = request.json(&json_body);
93        }
94
95        let response = request.send()?;
96        let status = response.status();
97        let payload = response.text()?;
98
99        if !status.is_success() {
100            return Err(ClientError::HttpStatus {
101                status,
102                body: payload,
103            });
104        }
105
106        if payload.trim().is_empty() {
107            Ok(Value::Null)
108        } else {
109            Ok(serde_json::from_str(&payload)?)
110        }
111    }
112
113    fn build_url(&self, path: &str) -> Result<Url, ClientError> {
114        let relative = path.trim_start_matches('/');
115        self.base_url
116            .join(relative)
117            .map_err(|_| ClientError::InvalidPath(path.to_owned()))
118    }
119}
120
121fn ensure_trailing_slash(mut url: Url) -> Url {
122    if !url.path().ends_with('/') {
123        let mut path = url.path().to_owned();
124        path.push('/');
125        url.set_path(&path);
126    }
127    url
128}