use reqwest::Method;
use serde_json::Value;
use url::form_urlencoded::byte_serialize;
use crate::{ApiClient, BlockingApiClient, ClientError};
#[derive(Clone, Copy, Debug)]
pub struct OperationDefinition {
pub operation_id: &'static str,
pub method: &'static str,
pub path_template: &'static str,
pub path_params: &'static [&'static str],
}
include!(concat!(env!("OUT_DIR"), "/openapi_operations.rs"));
#[derive(Clone, Debug)]
pub struct IriClient {
inner: ApiClient,
}
impl IriClient {
pub fn new(base_url: impl AsRef<str>) -> Result<Self, ClientError> {
Ok(Self {
inner: ApiClient::new(base_url)?,
})
}
pub fn from_openapi_default_server() -> Result<Self, ClientError> {
Self::new(openapi_default_server_url())
}
#[must_use]
pub fn with_authorization_token(mut self, token: impl Into<String>) -> Self {
self.inner = self.inner.with_authorization_token(token);
self
}
pub fn operations() -> &'static [OperationDefinition] {
OPENAPI_OPERATIONS
}
pub async fn request_json_with_query(
&self,
method: Method,
path: &str,
query: &[(&str, &str)],
body: Option<Value>,
) -> Result<Value, ClientError> {
self.inner
.request_json_with_query(method, path, query, body)
.await
}
pub async fn call_operation(
&self,
operation_id: &str,
path_params: &[(&str, &str)],
query: &[(&str, &str)],
body: Option<Value>,
) -> Result<Value, ClientError> {
let operation = find_operation(operation_id)?;
let rendered_path = render_path(operation, path_params)?;
let method = parse_method(operation)?;
self.inner
.request_json_with_query(method, &rendered_path, query, body)
.await
}
}
#[derive(Debug)]
pub struct BlockingIriClient {
inner: BlockingApiClient,
}
impl BlockingIriClient {
pub fn new(base_url: impl AsRef<str>) -> Result<Self, ClientError> {
Ok(Self {
inner: BlockingApiClient::new(base_url)?,
})
}
pub fn from_openapi_default_server() -> Result<Self, ClientError> {
Self::new(openapi_default_server_url())
}
#[must_use]
pub fn with_authorization_token(mut self, token: impl Into<String>) -> Self {
self.inner = self.inner.with_authorization_token(token);
self
}
pub fn operations() -> &'static [OperationDefinition] {
OPENAPI_OPERATIONS
}
pub fn request_json_with_query(
&self,
method: Method,
path: &str,
query: &[(&str, &str)],
body: Option<Value>,
) -> Result<Value, ClientError> {
self.inner
.request_json_with_query(method, path, query, body)
}
pub fn call_operation(
&self,
operation_id: &str,
path_params: &[(&str, &str)],
query: &[(&str, &str)],
body: Option<Value>,
) -> Result<Value, ClientError> {
let operation = find_operation(operation_id)?;
let rendered_path = render_path(operation, path_params)?;
let method = parse_method(operation)?;
self.inner
.request_json_with_query(method, &rendered_path, query, body)
}
}
pub fn openapi_default_server_url() -> &'static str {
OPENAPI_DEFAULT_SERVER_URL
}
fn find_operation(operation_id: &str) -> Result<&'static OperationDefinition, ClientError> {
OPENAPI_OPERATIONS
.iter()
.find(|op| op.operation_id == operation_id)
.ok_or_else(|| ClientError::UnknownOperation(operation_id.to_owned()))
}
fn parse_method(operation: &OperationDefinition) -> Result<Method, ClientError> {
Method::from_bytes(operation.method.as_bytes())
.map_err(|_| ClientError::UnknownOperation(operation.operation_id.to_owned()))
}
fn render_path(
operation: &OperationDefinition,
path_params: &[(&str, &str)],
) -> Result<String, ClientError> {
let mut rendered = operation.path_template.to_owned();
for required_param in operation.path_params {
let value = path_params
.iter()
.find(|(name, _)| name == required_param)
.map(|(_, value)| *value)
.ok_or_else(|| ClientError::MissingPathParameter {
operation_id: operation.operation_id.to_owned(),
parameter: (*required_param).to_owned(),
})?;
let placeholder = format!("{{{required_param}}}");
rendered = rendered.replace(&placeholder, &encode_path_segment(value));
}
Ok(rendered)
}
fn encode_path_segment(value: &str) -> String {
byte_serialize(value.as_bytes()).collect()
}
#[cfg(test)]
mod tests {
use super::{IriClient, find_operation, render_path};
use crate::ClientError;
#[test]
fn operation_catalog_is_non_empty() {
assert!(!IriClient::operations().is_empty());
}
#[test]
fn render_path_replaces_required_path_params() {
let op = find_operation("getSite").expect("operation exists");
let path = render_path(op, &[("site_id", "site-1")]).expect("path renders");
assert_eq!(path, "/api/v1/facility/sites/site-1");
}
#[test]
fn render_path_reports_missing_parameter() {
let op = find_operation("getSite").expect("operation exists");
let error = render_path(op, &[]).expect_err("missing parameter should error");
match error {
ClientError::MissingPathParameter {
operation_id,
parameter,
} => {
assert_eq!(operation_id, "getSite");
assert_eq!(parameter, "site_id");
}
other => panic!("unexpected error: {other}"),
}
}
}