use core::fmt;
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use cloud_sdk::Method;
use cloud_sdk::authentication::{
AsyncAuthenticatedTransport, AuthenticatedRequest, BlockingAuthenticatedTransport,
};
use cloud_sdk::transport::{
AsyncResponseStaging, AsyncTransport, BlockingTransport, BoundTransport, EndpointIdentity,
EndpointIdentityError, RequestHeaders, RequestTarget, ResponseCompletion, ResponseWriter,
TransportRequest,
};
use crate::mock::{MockResponseSink, stage_response};
use crate::{MAX_DYNAMIC_RECORDS, MockError, RequestRecordSlot, ResponseFixture};
#[derive(Clone, Copy)]
pub struct DynamicRequest<'request> {
sequence: usize,
request: TransportRequest<'request>,
}
impl<'request> DynamicRequest<'request> {
const fn new(sequence: usize, request: TransportRequest<'request>) -> Self {
Self { sequence, request }
}
#[must_use]
pub const fn sequence(self) -> usize {
self.sequence
}
#[must_use]
pub const fn method(self) -> Method {
self.request.method()
}
#[must_use]
pub const fn target(self) -> RequestTarget<'request> {
self.request.target()
}
#[must_use]
pub const fn body(self) -> &'request [u8] {
self.request.body()
}
#[must_use]
pub const fn headers(self) -> RequestHeaders<'request> {
self.request.headers()
}
}
impl fmt::Debug for DynamicRequest<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DynamicRequest")
.field("sequence", &self.sequence)
.field("method", &self.request.method())
.field("target", &"[redacted]")
.field("body", &"[redacted]")
.field("headers", &self.request.headers())
.finish()
}
}
pub trait ProviderFixtureBuilder<'fixture> {
type Error;
fn build<'request>(
&self,
request: DynamicRequest<'request>,
) -> Result<&'fixture ResponseFixture<'fixture>, Self::Error>;
}
pub struct DynamicResponder<F> {
responder: F,
}
impl<F> DynamicResponder<F> {
#[must_use]
pub const fn new(responder: F) -> Self {
Self { responder }
}
}
impl<'fixture, E, F> ProviderFixtureBuilder<'fixture> for DynamicResponder<F>
where
F: for<'request> Fn(DynamicRequest<'request>) -> Result<&'fixture ResponseFixture<'fixture>, E>,
{
type Error = E;
fn build<'request>(
&self,
request: DynamicRequest<'request>,
) -> Result<&'fixture ResponseFixture<'fixture>, Self::Error> {
(self.responder)(request)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DynamicMockConfigError {
NoRecordSlots,
TooManyRecordSlots,
DirtyRecordSlot,
}
impl_static_error!(DynamicMockConfigError,
Self::NoRecordSlots => "dynamic mock requires at least one record slot",
Self::TooManyRecordSlots => "dynamic mock recording capacity exceeds the limit",
Self::DirtyRecordSlot => "dynamic mock record slots are not empty",
);
pub enum DynamicMockError<E> {
Exhausted,
ConcurrentRequest,
Builder(E),
Fixture(MockError),
CursorOverflow,
}
impl<E> fmt::Debug for DynamicMockError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Exhausted => "DynamicMockError::Exhausted",
Self::ConcurrentRequest => "DynamicMockError::ConcurrentRequest",
Self::Builder(_) => "DynamicMockError::Builder([redacted])",
Self::Fixture(_) => "DynamicMockError::Fixture([redacted])",
Self::CursorOverflow => "DynamicMockError::CursorOverflow",
})
}
}
impl<E> fmt::Display for DynamicMockError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Exhausted => "dynamic mock recording capacity is exhausted",
Self::ConcurrentRequest => "dynamic mock request overlaps another request",
Self::Builder(_) => "dynamic fixture builder rejected the request",
Self::Fixture(_) => "dynamic response fixture could not be staged",
Self::CursorOverflow => "dynamic mock cursor overflowed",
})
}
}
impl<E> core::error::Error for DynamicMockError<E> {}
pub struct DynamicMockTransport<'fixture, 'records, B> {
builder: B,
records: &'records [RequestRecordSlot],
cursor: AtomicUsize,
in_flight: AtomicBool,
endpoint: Option<EndpointIdentity<'fixture>>,
}
impl<'fixture, 'records, B> DynamicMockTransport<'fixture, 'records, B> {
pub fn new(
builder: B,
records: &'records [RequestRecordSlot],
) -> Result<Self, DynamicMockConfigError> {
if records.is_empty() {
return Err(DynamicMockConfigError::NoRecordSlots);
}
if records.len() > MAX_DYNAMIC_RECORDS {
return Err(DynamicMockConfigError::TooManyRecordSlots);
}
if records.iter().any(|slot| !slot.is_empty()) {
return Err(DynamicMockConfigError::DirtyRecordSlot);
}
Ok(Self {
builder,
records,
cursor: AtomicUsize::new(0),
in_flight: AtomicBool::new(false),
endpoint: None,
})
}
#[must_use]
pub const fn with_endpoint(mut self, endpoint: EndpointIdentity<'fixture>) -> Self {
self.endpoint = Some(endpoint);
self
}
#[must_use]
pub fn recorded(&self) -> usize {
self.cursor.load(Ordering::Acquire)
}
#[must_use]
pub const fn capacity(&self) -> usize {
self.records.len()
}
#[must_use]
pub fn record(&self, index: usize) -> Option<crate::RecordedRequest> {
self.records.get(index).and_then(RequestRecordSlot::get)
}
}
struct InFlightGuard<'a>(&'a AtomicBool);
impl Drop for InFlightGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
impl<'fixture, B> DynamicMockTransport<'fixture, '_, B>
where
B: ProviderFixtureBuilder<'fixture>,
{
fn stage_inner<'buffer>(
&self,
request: TransportRequest<'_>,
response: &mut impl MockResponseSink<'buffer>,
) -> Result<ResponseCompletion, DynamicMockError<B::Error>> {
self.in_flight
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.map_err(|_| DynamicMockError::ConcurrentRequest)?;
let _guard = InFlightGuard(&self.in_flight);
let sequence = self.cursor.load(Ordering::Acquire);
let slot = self
.records
.get(sequence)
.ok_or(DynamicMockError::Exhausted)?;
let fixture = self
.builder
.build(DynamicRequest::new(sequence, request))
.map_err(DynamicMockError::Builder)?;
let completion = stage_response(fixture, response).map_err(DynamicMockError::Fixture)?;
let next = sequence
.checked_add(1)
.ok_or(DynamicMockError::CursorOverflow)?;
slot.commit(sequence, request, fixture.status());
self.cursor.store(next, Ordering::Release);
Ok(completion)
}
fn send_inner(
&self,
request: TransportRequest<'_>,
response: &mut ResponseWriter<'_>,
) -> Result<(), DynamicMockError<B::Error>> {
if response.is_committed() {
return Err(DynamicMockError::Fixture(MockError::ResponseWriterRejected));
}
let mut attempt = response
.begin_attempt()
.map_err(|_| DynamicMockError::Fixture(MockError::ResponseWriterRejected))?;
let completion = self.stage_inner(request, &mut attempt)?;
attempt
.commit_completion(completion)
.map_err(|_| DynamicMockError::Fixture(MockError::ResponseWriterRejected))
}
}
impl<'fixture, B> BlockingTransport for DynamicMockTransport<'fixture, '_, B>
where
B: ProviderFixtureBuilder<'fixture>,
{
type Error = DynamicMockError<B::Error>;
fn send(
&self,
request: TransportRequest<'_>,
response: &mut ResponseWriter<'_>,
) -> Result<(), Self::Error> {
self.send_inner(request, response)
}
}
impl<'fixture, B> BlockingAuthenticatedTransport for DynamicMockTransport<'fixture, '_, B>
where
B: ProviderFixtureBuilder<'fixture>,
{
type Error = DynamicMockError<B::Error>;
fn send_authenticated(
&self,
request: AuthenticatedRequest<'_, '_>,
response: &mut ResponseWriter<'_>,
) -> Result<(), Self::Error> {
self.send_inner(request.transport_request(), response)
}
}
impl<'fixture, B> AsyncTransport for DynamicMockTransport<'fixture, '_, B>
where
B: ProviderFixtureBuilder<'fixture> + Sync,
{
type Error = DynamicMockError<B::Error>;
async fn send<'transport, 'request, 'writer, 'buffer>(
&'transport self,
request: TransportRequest<'request>,
mut response: AsyncResponseStaging<'writer, 'buffer>,
) -> Result<ResponseCompletion, Self::Error>
where
'transport: 'writer,
'request: 'writer,
'buffer: 'writer,
{
self.stage_inner(request, &mut response)
}
}
impl<'fixture, B> AsyncAuthenticatedTransport for DynamicMockTransport<'fixture, '_, B>
where
B: ProviderFixtureBuilder<'fixture> + Sync,
{
type Error = DynamicMockError<B::Error>;
async fn send_authenticated<'transport, 'request, 'policy, 'writer, 'buffer>(
&'transport self,
request: AuthenticatedRequest<'request, 'policy>,
mut response: AsyncResponseStaging<'writer, 'buffer>,
) -> Result<ResponseCompletion, Self::Error>
where
'transport: 'writer,
'request: 'writer,
'policy: 'writer,
'buffer: 'writer,
{
self.stage_inner(request.transport_request(), &mut response)
}
}
impl<B> BoundTransport for DynamicMockTransport<'_, '_, B> {
fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, EndpointIdentityError> {
self.endpoint.ok_or(EndpointIdentityError::UnboundTransport)
}
}
impl<B> fmt::Debug for DynamicMockTransport<'_, '_, B> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DynamicMockTransport")
.field("recorded", &self.recorded())
.field("capacity", &self.capacity())
.finish_non_exhaustive()
}
}