use std::time::SystemTime;
use crate::bridge::file_source::{ErrorReason, RawResponse};
use super::{from_epoch, to_epoch};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Error {
pub reason: ErrorReason,
pub message: String,
pub retry_after: Option<SystemTime>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Response {
pub error: Option<Error>,
pub no_content: bool,
pub not_modified: bool,
pub must_revalidate: bool,
pub data: Option<Vec<u8>>,
pub modified: Option<SystemTime>,
pub expires: Option<SystemTime>,
pub etag: Option<String>,
}
impl Response {
#[must_use]
pub fn data(data: Vec<u8>) -> Self {
Self { data: Some(data), ..Self::default() }
}
#[must_use]
pub fn no_content() -> Self {
Self { no_content: true, ..Self::default() }
}
#[must_use]
pub fn not_modified() -> Self {
Self { not_modified: true, ..Self::default() }
}
#[must_use]
pub fn error(reason: ErrorReason, message: impl Into<String>) -> Self {
let reason = if reason == ErrorReason::Success { ErrorReason::Other } else { reason };
Self {
error: Some(Error { reason, message: message.into(), retry_after: None }),
..Self::default()
}
}
#[must_use]
pub fn with_retry_after(mut self, retry_after: SystemTime) -> Self {
if let Some(error) = &mut self.error {
error.retry_after = Some(retry_after);
}
self
}
#[must_use]
pub fn with_expires(mut self, expires: SystemTime) -> Self {
self.expires = Some(expires);
self
}
#[must_use]
pub fn with_modified(mut self, modified: SystemTime) -> Self {
self.modified = Some(modified);
self
}
#[must_use]
pub fn with_etag(mut self, etag: impl Into<String>) -> Self {
self.etag = Some(etag.into());
self
}
#[must_use]
pub fn with_must_revalidate(mut self, must_revalidate: bool) -> Self {
self.must_revalidate = must_revalidate;
self
}
pub(super) fn into_ffi(self) -> RawResponse {
let (has_error, error_reason, error_message, has_retry_after, retry_after_epoch_s) =
match self.error {
Some(Error { reason, message, retry_after }) => {
let reason =
if reason == ErrorReason::Success { ErrorReason::Other } else { reason };
let (has_retry_after, retry_after_epoch_s) = to_epoch(retry_after);
(true, reason, message, has_retry_after, retry_after_epoch_s)
}
None => (false, ErrorReason::Success, String::new(), false, 0),
};
let (has_data, data) = match self.data {
Some(data) => (true, data),
None => (false, Vec::new()),
};
let (has_modified, modified_epoch_s) = to_epoch(self.modified);
let (has_expires, expires_epoch_s) = to_epoch(self.expires);
let (has_etag, etag) = match self.etag {
Some(etag) => (true, etag),
None => (false, String::new()),
};
RawResponse {
has_error,
error_reason,
error_message,
has_retry_after,
retry_after_epoch_s,
no_content: self.no_content,
not_modified: self.not_modified,
must_revalidate: self.must_revalidate,
has_data,
data,
has_modified,
modified_epoch_s,
has_expires,
expires_epoch_s,
has_etag,
etag,
}
}
pub(super) fn from_ffi(ffi: &RawResponse) -> Self {
Self {
error: ffi.has_error.then(|| Error {
reason: ffi.error_reason,
message: ffi.error_message.clone(),
retry_after: from_epoch(ffi.has_retry_after, ffi.retry_after_epoch_s),
}),
no_content: ffi.no_content,
not_modified: ffi.not_modified,
must_revalidate: ffi.must_revalidate,
data: ffi.has_data.then(|| ffi.data.clone()),
modified: from_epoch(ffi.has_modified, ffi.modified_epoch_s),
expires: from_epoch(ffi.has_expires, ffi.expires_epoch_s),
etag: ffi.has_etag.then(|| ffi.etag.clone()),
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use crate::bridge::file_source::roundtrip_response_for_test;
use super::{ErrorReason, Response};
fn roundtrip(response: Response) -> Response {
let raw = roundtrip_response_for_test(&response.into_ffi());
Response::from_ffi(&raw)
}
#[test]
fn data_response_roundtrips_all_metadata() {
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(500_000);
let expires = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
let back = roundtrip(
Response::data(b"hello".to_vec())
.with_etag("etag-1")
.with_modified(modified)
.with_expires(expires)
.with_must_revalidate(true),
);
assert!(back.error.is_none());
assert_eq!(back.data.as_deref(), Some(b"hello".as_slice()));
assert_eq!(back.etag.as_deref(), Some("etag-1"));
assert_eq!(back.modified, Some(modified));
assert_eq!(back.expires, Some(expires));
assert!(back.must_revalidate);
assert!(!back.no_content);
assert!(!back.not_modified);
}
#[test]
fn no_content_and_not_modified_roundtrip() {
let back = roundtrip(Response::no_content());
assert!(back.no_content);
assert!(back.data.is_none());
assert!(back.error.is_none());
let back = roundtrip(Response::not_modified());
assert!(back.not_modified);
assert!(back.data.is_none());
}
#[test]
fn empty_body_stays_present_with_absent_metadata() {
let back = roundtrip(Response::data(Vec::new()));
assert_eq!(back.data.as_deref(), Some([].as_slice()));
assert!(back.modified.is_none());
assert!(back.expires.is_none());
assert!(back.etag.is_none());
assert!(!back.must_revalidate);
}
#[test]
fn pre_unix_epoch_timestamps_roundtrip() {
let modified = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
let back = roundtrip(Response::data(Vec::new()).with_modified(modified));
assert_eq!(back.modified, Some(modified));
}
#[test]
fn error_roundtrips_and_success_is_coerced() {
let retry_after = SystemTime::UNIX_EPOCH + Duration::from_secs(123_456);
let back = roundtrip(
Response::error(ErrorReason::RateLimit, "retry later").with_retry_after(retry_after),
);
let error = back.error.expect("error should be preserved");
assert_eq!(error.reason, ErrorReason::RateLimit);
assert_eq!(error.message, "retry later");
assert_eq!(error.retry_after, Some(retry_after));
assert!(back.data.is_none());
let back = roundtrip(Response::error(ErrorReason::Success, "oops"));
assert_eq!(back.error.expect("error should be preserved").reason, ErrorReason::Other);
}
}