use core::fmt;
use cloud_sdk::Method;
use cloud_sdk::transport::{
AsyncTransport, BlockingTransport, RequestTarget, TransportRequest, TransportResponse,
};
use crate::{FixtureBodyError, ResponseFixture};
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct ExpectedRequest<'a> {
method: Method,
target: RequestTarget<'a>,
body: &'a [u8],
}
impl<'a> ExpectedRequest<'a> {
#[must_use]
pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
Self {
method,
target,
body: &[],
}
}
#[must_use]
pub const fn with_body(mut self, body: &'a [u8]) -> Self {
self.body = body;
self
}
const fn method(self) -> Method {
self.method
}
const fn target(self) -> RequestTarget<'a> {
self.target
}
const fn body(self) -> &'a [u8] {
self.body
}
}
impl fmt::Debug for ExpectedRequest<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ExpectedRequest")
.field("method", &self.method)
.field("target", &"[redacted]")
.field("body", &"[redacted]")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MockExchange<'a> {
request: ExpectedRequest<'a>,
response: ResponseFixture<'a>,
}
impl<'a> MockExchange<'a> {
#[must_use]
pub const fn new(request: ExpectedRequest<'a>, response: ResponseFixture<'a>) -> Self {
Self { request, response }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MockError {
Exhausted,
MethodMismatch,
TargetMismatch,
BodyMismatch,
ResponseBufferTooSmall,
CursorOverflow,
InvalidFixtureMetadata,
}
pub struct MockTransport<'a> {
exchanges: &'a [MockExchange<'a>],
cursor: usize,
}
impl<'a> MockTransport<'a> {
#[must_use]
pub const fn new(exchanges: &'a [MockExchange<'a>]) -> Self {
Self {
exchanges,
cursor: 0,
}
}
#[must_use]
pub const fn remaining(&self) -> usize {
self.exchanges.len().saturating_sub(self.cursor)
}
#[must_use]
pub const fn is_complete(&self) -> bool {
self.remaining() == 0
}
fn send_inner<'buffer>(
&mut self,
request: TransportRequest<'_>,
response_body: &'buffer mut [u8],
) -> Result<TransportResponse<'buffer>, MockError> {
let exchange = self
.exchanges
.get(self.cursor)
.ok_or(MockError::Exhausted)?;
if request.method() != exchange.request.method() {
return Err(MockError::MethodMismatch);
}
if request.target() != exchange.request.target() {
return Err(MockError::TargetMismatch);
}
if request.body() != exchange.request.body() {
return Err(MockError::BodyMismatch);
}
let next_cursor = self
.cursor
.checked_add(1)
.ok_or(MockError::CursorOverflow)?;
let body_len =
exchange
.response
.body()
.write_to(response_body)
.map_err(|error| match error {
FixtureBodyError::OutputTooSmall | FixtureBodyError::TooLarge => {
MockError::ResponseBufferTooSmall
}
})?;
let initialized = response_body
.get(..body_len)
.ok_or(MockError::ResponseBufferTooSmall)?;
self.cursor = next_cursor;
let response = TransportResponse::new(exchange.response.status(), initialized);
let rate_limit = exchange
.response
.rate_limit()
.map(|value| value.into_rate_limit())
.transpose()
.map_err(|_| MockError::InvalidFixtureMetadata)?;
Ok(rate_limit.map_or(response, |value| response.with_rate_limit(value)))
}
}
impl BlockingTransport for MockTransport<'_> {
type Error = MockError;
fn send<'buffer>(
&mut self,
request: TransportRequest<'_>,
response_body: &'buffer mut [u8],
) -> Result<TransportResponse<'buffer>, Self::Error> {
self.send_inner(request, response_body)
}
}
impl AsyncTransport for MockTransport<'_> {
type Error = MockError;
async fn send<'transport, 'request, 'buffer>(
&'transport mut self,
request: TransportRequest<'request>,
response_body: &'buffer mut [u8],
) -> Result<TransportResponse<'buffer>, Self::Error>
where
'request: 'transport,
'buffer: 'transport,
{
self.send_inner(request, response_body)
}
}
impl fmt::Debug for MockTransport<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("MockTransport")
.field("remaining", &self.remaining())
.finish_non_exhaustive()
}
}