use std::any::{TypeId, type_name};
use headers::{ContentType, Header};
use http::StatusCode;
use http::header::CONTENT_TYPE;
use reqwest::Response;
use serde::de::DeserializeOwned;
use utoipa::ToSchema;
use utoipa::openapi::{RefOr, Schema};
use super::channel::{CollectorMessage, CollectorSender};
use super::schema::{SchemaEntry, compute_schema_ref};
use crate::client::ApiClientError;
use crate::client::response::output::Output;
#[derive(Debug, Clone)]
pub struct CallResult {
operation_id: String,
status: StatusCode,
content_type: Option<ContentType>,
output: Output,
pub(in crate::client) collector_sender: CollectorSender,
}
#[derive(Debug, Clone)]
pub struct RawResult {
status: StatusCode,
content_type: Option<String>,
body: RawBody,
}
#[derive(Debug, Clone)]
pub enum RawBody {
Text(String),
Binary(Vec<u8>),
Empty,
}
impl RawResult {
pub fn status_code(&self) -> StatusCode {
self.status
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub fn body(&self) -> &RawBody {
&self.body
}
pub fn text(&self) -> Option<&str> {
match &self.body {
RawBody::Text(text) => Some(text),
_ => None,
}
}
pub fn bytes(&self) -> Option<&[u8]> {
match &self.body {
RawBody::Binary(bytes) => Some(bytes),
_ => None,
}
}
pub fn is_empty(&self) -> bool {
matches!(self.body, RawBody::Empty)
}
}
impl CallResult {
#[cfg(feature = "redaction")]
pub(in crate::client) fn status(&self) -> StatusCode {
self.status
}
#[cfg(feature = "redaction")]
pub(in crate::client) fn content_type(&self) -> Option<&ContentType> {
self.content_type.as_ref()
}
#[cfg(feature = "redaction")]
pub(in crate::client) fn operation_id(&self) -> &str {
&self.operation_id
}
#[cfg(feature = "redaction")]
pub(in crate::client) fn output(&self) -> &Output {
&self.output
}
fn extract_content_type(response: &Response) -> Result<Option<ContentType>, ApiClientError> {
let content_type = response
.headers()
.get_all(CONTENT_TYPE)
.iter()
.collect::<Vec<_>>();
if content_type.is_empty() {
Ok(None)
} else {
let ct = ContentType::decode(&mut content_type.into_iter())?;
Ok(Some(ct))
}
}
async fn process_response_body(
response: Response,
content_type: &Option<ContentType>,
status: StatusCode,
) -> Result<Output, ApiClientError> {
if let Some(content_type) = content_type
&& status != StatusCode::NO_CONTENT
{
if *content_type == ContentType::json() {
let json = response.text().await?;
Ok(Output::Json(json))
} else if *content_type == ContentType::octet_stream() {
let bytes = response.bytes().await?;
Ok(Output::Bytes(bytes.to_vec()))
} else if content_type.to_string().starts_with("text/") {
let text = response.text().await?;
Ok(Output::Text(text))
} else {
let body = response.text().await?;
Ok(Output::Other { body })
}
} else {
Ok(Output::Empty)
}
}
pub(in crate::client) async fn new(
operation_id: String,
collector_sender: CollectorSender,
response: Response,
) -> Result<Self, ApiClientError> {
let status = response.status();
let content_type = Self::extract_content_type(&response)?;
let output = Self::process_response_body(response, &content_type, status).await?;
Ok(Self {
operation_id,
status,
content_type,
output,
collector_sender,
})
}
pub(in crate::client) async fn new_without_collection(
response: Response,
) -> Result<Self, ApiClientError> {
let status = response.status();
let content_type = Self::extract_content_type(&response)?;
let output = Self::process_response_body(response, &content_type, status).await?;
Ok(Self {
operation_id: String::new(), status,
content_type,
output,
collector_sender: CollectorSender::dummy(),
})
}
pub(in crate::client) async fn get_output(
&self,
schema: Option<RefOr<Schema>>,
) -> Result<&Output, ApiClientError> {
if self.operation_id.is_empty() {
return Ok(&self.output);
}
let status_code = self.status.as_u16();
let description = format!("Status code {status_code}");
self.collector_sender
.send(CollectorMessage::RegisterResponse {
operation_id: self.operation_id.clone(),
status: self.status,
content_type: self.content_type.clone(),
schema,
description,
})
.await;
Ok(&self.output)
}
pub async fn as_json<T>(&mut self) -> Result<T, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
{
let schema = self.register_schema::<T>().await;
let output = self.get_output(Some(schema)).await?;
let Output::Json(json) = output else {
return Err(ApiClientError::UnsupportedJsonOutput {
output: output.clone(),
name: type_name::<T>(),
});
};
self.deserialize_and_record::<T>(json).await
}
pub async fn as_optional_json<T>(&mut self) -> Result<Option<T>, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
{
if self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND {
self.get_output(None).await?;
return Ok(None);
}
let schema = self.register_schema::<T>().await;
let output = self.get_output(Some(schema)).await?;
let Output::Json(json) = output else {
return Err(ApiClientError::UnsupportedJsonOutput {
output: output.clone(),
name: type_name::<T>(),
});
};
let result = self.deserialize_and_record::<T>(json).await?;
Ok(Some(result))
}
pub async fn as_result_json<T, E>(&mut self) -> Result<Result<T, E>, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
E: DeserializeOwned + ToSchema + 'static,
{
Ok(self
.process_result_json_internal::<T, E>(false)
.await?
.map(|opt| opt.expect("BUG: 404 handling disabled but got None")))
}
pub async fn as_result_option_json<T, E>(
&mut self,
) -> Result<Result<Option<T>, E>, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
E: DeserializeOwned + ToSchema + 'static,
{
self.process_result_json_internal::<T, E>(true).await
}
async fn process_result_json_internal<T, E>(
&mut self,
treat_404_as_none: bool,
) -> Result<Result<Option<T>, E>, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
E: DeserializeOwned + ToSchema + 'static,
{
let success_schema = self.register_schema::<T>().await;
let error_schema = self.register_schema::<E>().await;
if treat_404_as_none
&& (self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND)
{
self.get_output(None).await?;
return Ok(Ok(None));
}
let is_success = self.status.is_success();
let schema = if is_success {
success_schema
} else {
error_schema
};
let output = self.get_output(Some(schema)).await?;
let Output::Json(json) = output else {
return Err(ApiClientError::UnsupportedJsonOutput {
output: output.clone(),
name: if is_success {
type_name::<T>()
} else {
type_name::<E>()
},
});
};
if is_success {
let value = self.deserialize_and_record::<T>(json).await?;
Ok(Ok(Some(value)))
} else {
let error = self.deserialize_and_record::<E>(json).await?;
Ok(Err(error))
}
}
async fn register_schema<T: ToSchema + 'static>(&self) -> RefOr<Schema> {
let schema = compute_schema_ref::<T>();
self.collector_sender
.send(CollectorMessage::AddSchemaEntry(SchemaEntry::of::<T>()))
.await;
schema
}
async fn deserialize_and_record<T>(&self, json: &str) -> Result<T, ApiClientError>
where
T: DeserializeOwned + ToSchema + 'static,
{
let deserializer = &mut serde_json::Deserializer::from_str(json);
let result: T = serde_path_to_error::deserialize(deserializer).map_err(|err| {
ApiClientError::JsonError {
path: err.path().to_string(),
error: err.into_inner(),
body: json.to_string(),
}
})?;
if let Ok(example) = serde_json::to_value(json) {
self.collector_sender
.send(CollectorMessage::AddExample {
type_id: TypeId::of::<T>(),
type_name: type_name::<T>(),
example,
})
.await;
}
Ok(result)
}
pub async fn as_text(&mut self) -> Result<&str, ApiClientError> {
let output = self.get_output(None).await?;
let Output::Text(text) = &output else {
return Err(ApiClientError::UnsupportedTextOutput {
output: output.clone(),
});
};
Ok(text)
}
pub async fn as_bytes(&mut self) -> Result<&[u8], ApiClientError> {
let output = self.get_output(None).await?;
let Output::Bytes(bytes) = &output else {
return Err(ApiClientError::UnsupportedBytesOutput {
output: output.clone(),
});
};
Ok(bytes.as_slice())
}
pub async fn as_raw(&mut self) -> Result<RawResult, ApiClientError> {
let output = self.get_output(None).await?;
let body = match output {
Output::Empty => RawBody::Empty,
Output::Json(body) | Output::Text(body) | Output::Other { body, .. } => {
RawBody::Text(body.clone())
}
Output::Bytes(bytes) => RawBody::Binary(bytes.clone()),
};
Ok(RawResult {
status: self.status,
content_type: self.content_type.as_ref().map(|ct| ct.to_string()),
body,
})
}
pub async fn as_empty(&mut self) -> Result<(), ApiClientError> {
self.get_output(None).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_body_text_variant() {
let body = RawBody::Text("Hello, World!".to_string());
match body {
RawBody::Text(text) => assert_eq!(text, "Hello, World!"),
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_raw_body_binary_variant() {
let data = vec![0x01, 0x02, 0x03, 0x04];
let body = RawBody::Binary(data.clone());
match body {
RawBody::Binary(bytes) => assert_eq!(bytes, data),
_ => panic!("Expected Binary variant"),
}
}
#[test]
fn test_raw_body_empty_variant() {
let body = RawBody::Empty;
assert!(matches!(body, RawBody::Empty));
}
#[test]
fn test_raw_result_status_code() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/json".to_string()),
body: RawBody::Text("{}".to_string()),
};
assert_eq!(result.status_code(), StatusCode::OK);
}
#[test]
fn test_raw_result_content_type_some() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("text/plain".to_string()),
body: RawBody::Text("Hello".to_string()),
};
assert_eq!(result.content_type(), Some("text/plain"));
}
#[test]
fn test_raw_result_content_type_none() {
let result = RawResult {
status: StatusCode::NO_CONTENT,
content_type: None,
body: RawBody::Empty,
};
assert_eq!(result.content_type(), None);
}
#[test]
fn test_raw_result_body() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/json".to_string()),
body: RawBody::Text("{\"key\": \"value\"}".to_string()),
};
assert!(matches!(result.body(), RawBody::Text(_)));
}
#[test]
fn test_raw_result_text_with_text_body() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("text/plain".to_string()),
body: RawBody::Text("Hello, World!".to_string()),
};
assert_eq!(result.text(), Some("Hello, World!"));
}
#[test]
fn test_raw_result_text_with_binary_body() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/octet-stream".to_string()),
body: RawBody::Binary(vec![0x00, 0x01, 0x02]),
};
assert_eq!(result.text(), None);
}
#[test]
fn test_raw_result_text_with_empty_body() {
let result = RawResult {
status: StatusCode::NO_CONTENT,
content_type: None,
body: RawBody::Empty,
};
assert_eq!(result.text(), None);
}
#[test]
fn test_raw_result_bytes_with_binary_body() {
let data = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/octet-stream".to_string()),
body: RawBody::Binary(data.clone()),
};
assert_eq!(result.bytes(), Some(data.as_slice()));
}
#[test]
fn test_raw_result_bytes_with_text_body() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("text/plain".to_string()),
body: RawBody::Text("Hello".to_string()),
};
assert_eq!(result.bytes(), None);
}
#[test]
fn test_raw_result_bytes_with_empty_body() {
let result = RawResult {
status: StatusCode::NO_CONTENT,
content_type: None,
body: RawBody::Empty,
};
assert_eq!(result.bytes(), None);
}
#[test]
fn test_raw_result_is_empty_true() {
let result = RawResult {
status: StatusCode::NO_CONTENT,
content_type: None,
body: RawBody::Empty,
};
assert!(result.is_empty());
}
#[test]
fn test_raw_result_is_empty_false_text() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("text/plain".to_string()),
body: RawBody::Text("content".to_string()),
};
assert!(!result.is_empty());
}
#[test]
fn test_raw_result_is_empty_false_binary() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/octet-stream".to_string()),
body: RawBody::Binary(vec![1, 2, 3]),
};
assert!(!result.is_empty());
}
#[test]
fn test_raw_body_debug_impl() {
let text_body = RawBody::Text("Hello".to_string());
let debug_str = format!("{text_body:?}");
assert!(debug_str.contains("Text"));
assert!(debug_str.contains("Hello"));
let binary_body = RawBody::Binary(vec![1, 2, 3]);
let debug_str = format!("{binary_body:?}");
assert!(debug_str.contains("Binary"));
let empty_body = RawBody::Empty;
let debug_str = format!("{empty_body:?}");
assert!(debug_str.contains("Empty"));
}
#[test]
fn test_raw_body_clone() {
let original = RawBody::Text("test".to_string());
let cloned = original.clone();
match (original, cloned) {
(RawBody::Text(a), RawBody::Text(b)) => assert_eq!(a, b),
_ => panic!("Clone should preserve variant"),
}
}
#[test]
fn test_raw_result_debug_impl() {
let result = RawResult {
status: StatusCode::OK,
content_type: Some("application/json".to_string()),
body: RawBody::Text("{}".to_string()),
};
let debug_str = format!("{result:?}");
assert!(debug_str.contains("RawResult"));
assert!(debug_str.contains("200"));
}
#[test]
fn test_raw_result_clone() {
let original = RawResult {
status: StatusCode::CREATED,
content_type: Some("text/plain".to_string()),
body: RawBody::Text("Created".to_string()),
};
let cloned = original.clone();
assert_eq!(cloned.status_code(), StatusCode::CREATED);
assert_eq!(cloned.content_type(), Some("text/plain"));
assert_eq!(cloned.text(), Some("Created"));
}
}