1use reqwest::Method;
2use serde_json::Value;
3use url::form_urlencoded::byte_serialize;
4
5use crate::{ApiClient, BlockingApiClient, ClientError};
6
7#[derive(Clone, Copy, Debug)]
11pub struct OperationDefinition {
12 pub operation_id: &'static str,
14 pub method: &'static str,
16 pub path_template: &'static str,
18 pub path_params: &'static [&'static str],
20}
21
22include!(concat!(env!("OUT_DIR"), "/openapi_operations.rs"));
34
35#[derive(Clone, Debug)]
40pub struct IriClient {
41 inner: ApiClient,
42}
43
44impl IriClient {
45 pub fn new(base_url: impl AsRef<str>) -> Result<Self, ClientError> {
47 Ok(Self {
48 inner: ApiClient::new(base_url)?,
49 })
50 }
51
52 pub fn from_openapi_default_server() -> Result<Self, ClientError> {
54 Self::new(openapi_default_server_url())
55 }
56
57 #[must_use]
61 pub fn with_authorization_token(mut self, token: impl Into<String>) -> Self {
62 self.inner = self.inner.with_authorization_token(token);
63 self
64 }
65
66 pub fn operations() -> &'static [OperationDefinition] {
68 OPENAPI_OPERATIONS
69 }
70
71 pub async fn request_json_with_query(
75 &self,
76 method: Method,
77 path: &str,
78 query: &[(&str, &str)],
79 body: Option<Value>,
80 ) -> Result<Value, ClientError> {
81 self.inner
82 .request_json_with_query(method, path, query, body)
83 .await
84 }
85
86 pub async fn call_operation(
92 &self,
93 operation_id: &str,
94 path_params: &[(&str, &str)],
95 query: &[(&str, &str)],
96 body: Option<Value>,
97 ) -> Result<Value, ClientError> {
98 let operation = find_operation(operation_id)?;
99 let rendered_path = render_path(operation, path_params)?;
100 let method = parse_method(operation)?;
101 self.inner
102 .request_json_with_query(method, &rendered_path, query, body)
103 .await
104 }
105}
106
107#[derive(Debug)]
111pub struct BlockingIriClient {
112 inner: BlockingApiClient,
113}
114
115impl BlockingIriClient {
116 pub fn new(base_url: impl AsRef<str>) -> Result<Self, ClientError> {
118 Ok(Self {
119 inner: BlockingApiClient::new(base_url)?,
120 })
121 }
122
123 pub fn from_openapi_default_server() -> Result<Self, ClientError> {
125 Self::new(openapi_default_server_url())
126 }
127
128 #[must_use]
132 pub fn with_authorization_token(mut self, token: impl Into<String>) -> Self {
133 self.inner = self.inner.with_authorization_token(token);
134 self
135 }
136
137 pub fn operations() -> &'static [OperationDefinition] {
139 OPENAPI_OPERATIONS
140 }
141
142 pub fn request_json_with_query(
146 &self,
147 method: Method,
148 path: &str,
149 query: &[(&str, &str)],
150 body: Option<Value>,
151 ) -> Result<Value, ClientError> {
152 self.inner
153 .request_json_with_query(method, path, query, body)
154 }
155
156 pub fn call_operation(
162 &self,
163 operation_id: &str,
164 path_params: &[(&str, &str)],
165 query: &[(&str, &str)],
166 body: Option<Value>,
167 ) -> Result<Value, ClientError> {
168 let operation = find_operation(operation_id)?;
169 let rendered_path = render_path(operation, path_params)?;
170 let method = parse_method(operation)?;
171 self.inner
172 .request_json_with_query(method, &rendered_path, query, body)
173 }
174}
175
176pub fn openapi_default_server_url() -> &'static str {
180 OPENAPI_DEFAULT_SERVER_URL
181}
182
183fn find_operation(operation_id: &str) -> Result<&'static OperationDefinition, ClientError> {
184 OPENAPI_OPERATIONS
185 .iter()
186 .find(|op| op.operation_id == operation_id)
187 .ok_or_else(|| ClientError::UnknownOperation(operation_id.to_owned()))
188}
189
190fn parse_method(operation: &OperationDefinition) -> Result<Method, ClientError> {
191 Method::from_bytes(operation.method.as_bytes())
192 .map_err(|_| ClientError::UnknownOperation(operation.operation_id.to_owned()))
193}
194
195fn render_path(
196 operation: &OperationDefinition,
197 path_params: &[(&str, &str)],
198) -> Result<String, ClientError> {
199 let mut rendered = operation.path_template.to_owned();
200
201 for required_param in operation.path_params {
202 let value = path_params
203 .iter()
204 .find(|(name, _)| name == required_param)
205 .map(|(_, value)| *value)
206 .ok_or_else(|| ClientError::MissingPathParameter {
207 operation_id: operation.operation_id.to_owned(),
208 parameter: (*required_param).to_owned(),
209 })?;
210
211 let placeholder = format!("{{{required_param}}}");
212 rendered = rendered.replace(&placeholder, &encode_path_segment(value));
213 }
214
215 Ok(rendered)
216}
217
218fn encode_path_segment(value: &str) -> String {
219 byte_serialize(value.as_bytes()).collect()
220}
221
222#[cfg(test)]
223mod tests {
224 use super::{IriClient, find_operation, render_path};
225 use crate::ClientError;
226
227 #[test]
228 fn operation_catalog_is_non_empty() {
229 assert!(!IriClient::operations().is_empty());
230 }
231
232 #[test]
233 fn render_path_replaces_required_path_params() {
234 let op = find_operation("getSite").expect("operation exists");
235 let path = render_path(op, &[("site_id", "site-1")]).expect("path renders");
236 assert_eq!(path, "/api/v1/facility/sites/site-1");
237 }
238
239 #[test]
240 fn render_path_reports_missing_parameter() {
241 let op = find_operation("getSite").expect("operation exists");
242 let error = render_path(op, &[]).expect_err("missing parameter should error");
243 match error {
244 ClientError::MissingPathParameter {
245 operation_id,
246 parameter,
247 } => {
248 assert_eq!(operation_id, "getSite");
249 assert_eq!(parameter, "site_id");
250 }
251 other => panic!("unexpected error: {other}"),
252 }
253 }
254}