iri_client/
blocking_client.rs1use reqwest::{Method, Url};
2use serde_json::Value;
3
4use crate::ClientError;
5
6#[derive(Debug)]
10pub struct BlockingApiClient {
11 base_url: Url,
12 authorization_token: Option<String>,
13 http: reqwest::blocking::Client,
14}
15
16impl BlockingApiClient {
17 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 #[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 pub fn get_json(&self, path: &str) -> Result<Value, ClientError> {
43 self.request_json(Method::GET, path, None)
44 }
45
46 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 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 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}