use anyhow::{anyhow, Result};
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
use crate::{Cap, validation::ValidationError};
#[derive(Debug, Clone)]
pub struct ResponseWrapper {
raw_bytes: Vec<u8>,
content_type: ResponseContentType,
}
#[derive(Debug, Clone, PartialEq)]
enum ResponseContentType {
Json,
Text,
Binary,
}
impl ResponseWrapper {
pub fn from_json(data: Vec<u8>) -> Self {
Self {
raw_bytes: data,
content_type: ResponseContentType::Json,
}
}
pub fn from_text(data: Vec<u8>) -> Self {
Self {
raw_bytes: data,
content_type: ResponseContentType::Text,
}
}
pub fn from_binary(data: Vec<u8>) -> Self {
Self {
raw_bytes: data,
content_type: ResponseContentType::Binary,
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.raw_bytes
}
pub fn as_string(&self) -> Result<String> {
String::from_utf8(self.raw_bytes.clone())
.map_err(|e| anyhow!("Failed to convert response to string: {}", e))
}
pub fn as_int(&self) -> Result<i64> {
let text = self.as_string()?;
let trimmed = text.trim();
if let Ok(json_val) = serde_json::from_str::<JsonValue>(trimmed) {
if let Some(num) = json_val.as_i64() {
return Ok(num);
}
}
trimmed.parse::<i64>()
.map_err(|e| anyhow!("Failed to parse '{}' as integer: {}", trimmed, e))
}
pub fn as_float(&self) -> Result<f64> {
let text = self.as_string()?;
let trimmed = text.trim();
if let Ok(json_val) = serde_json::from_str::<JsonValue>(trimmed) {
if let Some(num) = json_val.as_f64() {
return Ok(num);
}
}
trimmed.parse::<f64>()
.map_err(|e| anyhow!("Failed to parse '{}' as float: {}", trimmed, e))
}
pub fn as_bool(&self) -> Result<bool> {
let text = self.as_string()?;
let trimmed = text.trim().to_lowercase();
match trimmed.as_str() {
"true" | "1" | "yes" | "y" => Ok(true),
"false" | "0" | "no" | "n" => Ok(false),
_ => {
if let Ok(json_val) = serde_json::from_str::<JsonValue>(&trimmed) {
if let Some(bool_val) = json_val.as_bool() {
return Ok(bool_val);
}
}
Err(anyhow!("Failed to parse '{}' as boolean", trimmed))
}
}
}
pub fn as_type<T: DeserializeOwned>(&self) -> Result<T> {
match self.content_type {
ResponseContentType::Json => {
let text = self.as_string()?;
serde_json::from_str(&text)
.map_err(|e| anyhow!("Failed to deserialize JSON response: {}\\nResponse: {}", e, text))
}
ResponseContentType::Text => {
let text = self.as_string()?;
serde_json::from_str(&format!("\"{}\"", text.replace("\"", "\\\"")))
.map_err(|e| anyhow!("Failed to deserialize text response as JSON string: {}\\nResponse: {}", e, text))
}
ResponseContentType::Binary => {
Err(anyhow!("Cannot deserialize binary response to structured type"))
}
}
}
pub fn is_empty(&self) -> bool {
self.raw_bytes.is_empty()
}
pub fn size(&self) -> usize {
self.raw_bytes.len()
}
pub fn validate_against_cap(&self, cap: &Cap) -> Result<(), ValidationError> {
let media_specs = cap.get_media_specs();
let _json_value = match self.content_type {
ResponseContentType::Json => {
let text = self.as_string().map_err(|e| {
ValidationError::JsonParseError {
cap_urn: cap.urn_string(),
error: format!("Failed to convert response to string: {}", e),
}
})?;
serde_json::from_str::<JsonValue>(&text).map_err(|e| {
ValidationError::JsonParseError {
cap_urn: cap.urn_string(),
error: format!("Failed to parse JSON: {}", e),
}
})?
},
ResponseContentType::Text => {
let text = self.as_string().map_err(|e| {
ValidationError::JsonParseError {
cap_urn: cap.urn_string(),
error: format!("Failed to convert response to string: {}", e),
}
})?;
JsonValue::String(text)
},
ResponseContentType::Binary => {
if let Some(output_def) = cap.get_output() {
let is_binary = output_def.is_binary(media_specs)
.map_err(|e| ValidationError::InvalidMediaSpec {
cap_urn: cap.urn_string(),
field_name: "output".to_string(),
error: e.to_string(),
})?;
if !is_binary {
return Err(ValidationError::InvalidOutputType {
cap_urn: cap.urn_string(),
expected_media_spec: output_def.media_urn.clone(),
actual_value: JsonValue::String(format!("{} bytes of binary data", self.raw_bytes.len())),
schema_errors: vec!["Expected non-binary output but received binary data".to_string()],
});
}
}
return Ok(());
}
};
Ok(())
}
pub fn get_content_type(&self) -> &str {
match self.content_type {
ResponseContentType::Json => "application/json",
ResponseContentType::Text => "text/plain",
ResponseContentType::Binary => "application/octet-stream",
}
}
pub fn matches_output_type(&self, cap: &Cap) -> Result<bool, crate::media_spec::MediaSpecError> {
let output_def = cap.get_output()
.ok_or_else(|| crate::media_spec::MediaSpecError::UnresolvableMediaUrn(
"cap has no output definition".to_string()
))?;
let media_specs = cap.get_media_specs();
let is_output_binary = output_def.is_binary(media_specs)?;
let is_output_json = output_def.is_json(media_specs)?;
Ok(match &self.content_type {
ResponseContentType::Json => is_output_json || !is_output_binary,
ResponseContentType::Text => !is_output_binary && !is_output_json,
ResponseContentType::Binary => is_output_binary,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestStruct {
name: String,
value: i32,
}
#[test]
fn test_json_response() {
let test_data = TestStruct {
name: "test".to_string(),
value: 42,
};
let json_str = serde_json::to_string(&test_data).unwrap();
let response = ResponseWrapper::from_json(json_str.into_bytes());
let parsed: TestStruct = response.as_type().unwrap();
assert_eq!(parsed, test_data);
}
#[test]
fn test_primitive_types() {
let response = ResponseWrapper::from_text(b"42".to_vec());
assert_eq!(response.as_int().unwrap(), 42);
let response = ResponseWrapper::from_text(b"3.14".to_vec());
assert_eq!(response.as_float().unwrap(), 3.14);
let response = ResponseWrapper::from_text(b"true".to_vec());
assert_eq!(response.as_bool().unwrap(), true);
let response = ResponseWrapper::from_text(b"hello world".to_vec());
assert_eq!(response.as_string().unwrap(), "hello world");
}
#[test]
fn test_binary_response() {
let binary_data = vec![0x89, 0x50, 0x4E, 0x47]; let response = ResponseWrapper::from_binary(binary_data.clone());
assert_eq!(response.as_bytes(), &binary_data);
assert_eq!(response.size(), 4);
}
}