saddle-framework 0.3.6

The single business-facing facade for Saddle applications
use crate::programming::{FakeProfuseContractBoundary, ProfuseGwDispatchError};
use std::future::Future;
use std::pin::Pin;

pub const PROFUSEGW_METHOD: &str = saddle_boundary::ingress::METHOD;
pub const PROFUSEGW_PATH: &str = saddle_boundary::ingress::PATH;
pub const PROFUSEGW_MEDIA_TYPE: &str = saddle_boundary::ingress::MEDIA_TYPE;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TestIngressIdentity {
    request_id: String,
    call_id: String,
    deadline_unix_ms: i64,
}

impl TestIngressIdentity {
    pub fn new(
        request_id: impl Into<String>,
        call_id: impl Into<String>,
        deadline_unix_ms: i64,
    ) -> Result<Self, ProfuseGwTestError> {
        let boundary =
            saddle_boundary::ingress::IngressIdentity::new(request_id, call_id, deadline_unix_ms)
                .map_err(ProfuseGwTestError::from_codec)?;
        Ok(Self {
            request_id: boundary.request_id,
            call_id: boundary.call_id,
            deadline_unix_ms: boundary.deadline_unix_ms,
        })
    }

    pub fn request_id(&self) -> &str {
        &self.request_id
    }

    pub fn call_id(&self) -> &str {
        &self.call_id
    }

    pub const fn deadline_unix_ms(&self) -> i64 {
        self.deadline_unix_ms
    }

    fn into_boundary(self) -> saddle_boundary::ingress::IngressIdentity {
        saddle_boundary::ingress::IngressIdentity::new(
            self.request_id,
            self.call_id,
            self.deadline_unix_ms,
        )
        .expect("facade test identity was validated at construction")
    }
}

pub struct ProfuseGwTestRequest {
    method: String,
    path: String,
    media_type: String,
    identity: TestIngressIdentity,
    json_body: String,
}

impl ProfuseGwTestRequest {
    pub fn new(
        method: impl Into<String>,
        path: impl Into<String>,
        media_type: impl Into<String>,
        identity: TestIngressIdentity,
        json_body: impl Into<String>,
    ) -> Self {
        Self {
            method: method.into(),
            path: path.into(),
            media_type: media_type.into(),
            identity,
            json_body: json_body.into(),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfuseGwTestAttempt {
    count: usize,
    request_id: String,
    call_id: String,
    user_id: String,
    trace_id: String,
    rpc_id: String,
    zone: String,
    idc: String,
    env: String,
    function: String,
    deadline_unix_ms: i64,
}

impl ProfuseGwTestAttempt {
    pub const fn count(&self) -> usize {
        self.count
    }
    pub fn request_id(&self) -> &str {
        &self.request_id
    }
    pub fn call_id(&self) -> &str {
        &self.call_id
    }
    pub fn user_id(&self) -> &str {
        &self.user_id
    }
    pub fn function(&self) -> &str {
        &self.function
    }
    pub fn trace_id(&self) -> &str {
        &self.trace_id
    }
    pub fn rpc_id(&self) -> &str {
        &self.rpc_id
    }
    pub fn zone(&self) -> &str {
        &self.zone
    }
    pub fn idc(&self) -> &str {
        &self.idc
    }
    pub fn env(&self) -> &str {
        &self.env
    }
    pub const fn deadline_unix_ms(&self) -> i64 {
        self.deadline_unix_ms
    }
}

pub struct ProfuseGwTestExecution<Response> {
    response: Response,
    attempts: Vec<ProfuseGwTestAttempt>,
}

impl<Response> ProfuseGwTestExecution<Response> {
    pub fn response(&self) -> &Response {
        &self.response
    }
    pub fn into_response(self) -> Response {
        self.response
    }
    pub fn attempts(&self) -> &[ProfuseGwTestAttempt] {
        &self.attempts
    }

    /// Serializes the framework-owned closed response for business assertions.
    pub fn response_json(&self) -> Result<String, ProfuseGwTestError>
    where
        Response: serde::Serialize,
    {
        serde_json::to_string(&self.response).map_err(|_| ProfuseGwTestError {
            code: "TEST_RESPONSE_JSON_INVALID",
        })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProfuseGwTestError {
    code: &'static str,
}

impl ProfuseGwTestError {
    pub const fn code(&self) -> &'static str {
        self.code
    }

    fn from_codec(error: saddle_boundary::ingress::CodecError) -> Self {
        Self { code: error.code }
    }

    fn from_dispatch(error: ProfuseGwDispatchError) -> Self {
        let code = match error {
            ProfuseGwDispatchError::InterfaceNotFound => "INTERFACE_NOT_FOUND",
            ProfuseGwDispatchError::RequestDataInvalid => "REQUEST_DATA_INVALID",
            ProfuseGwDispatchError::ContextInvalid => "CONTEXT_INVALID",
            ProfuseGwDispatchError::IdentityMismatch => "IDENTITY_MISMATCH",
        };
        Self { code }
    }
}

type TestFuture<Response> = Pin<
    Box<dyn Future<Output = Result<ProfuseGwTestExecution<Response>, ProfuseGwTestError>> + Send>,
>;
type TestExecutor<Response> = Box<dyn FnOnce(ProfuseGwTestRequest) -> TestFuture<Response> + Send>;

pub struct ProfuseGwTestHarness<Response> {
    executor: TestExecutor<Response>,
}

impl<Response> ProfuseGwTestHarness<Response> {
    #[doc(hidden)]
    pub fn from_executor(
        executor: impl FnOnce(ProfuseGwTestRequest) -> TestFuture<Response> + Send + 'static,
    ) -> Self {
        Self {
            executor: Box::new(executor),
        }
    }

    pub async fn execute(
        self,
        request: ProfuseGwTestRequest,
    ) -> Result<ProfuseGwTestExecution<Response>, ProfuseGwTestError> {
        (self.executor)(request).await
    }

    /// Executes the generated async handler graph on a framework-owned test
    /// runtime. Business test crates do not need a Tokio dependency.
    pub fn run(
        self,
        request: ProfuseGwTestRequest,
    ) -> Result<ProfuseGwTestExecution<Response>, ProfuseGwTestError> {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|_| ProfuseGwTestError {
                code: "TEST_RUNTIME_UNAVAILABLE",
            })?;
        runtime.block_on(self.execute(request))
    }
}

#[doc(hidden)]
pub fn accept_test_request(
    adapter: &saddle_boundary::ingress::ProfuseGwListenerAdapter,
    request: ProfuseGwTestRequest,
) -> Result<saddle_boundary::ingress::AcceptedIngress, ProfuseGwTestError> {
    adapter
        .accept(
            &request.method,
            &request.path,
            &request.media_type,
            request.identity.into_boundary(),
            request.json_body.as_bytes(),
        )
        .map_err(ProfuseGwTestError::from_codec)
}

#[doc(hidden)]
pub fn dispatch_error(error: ProfuseGwDispatchError) -> ProfuseGwTestError {
    ProfuseGwTestError::from_dispatch(error)
}

#[doc(hidden)]
pub fn execution<Response>(
    response: Response,
    fake: &FakeProfuseContractBoundary,
) -> ProfuseGwTestExecution<Response> {
    let attempts = fake
        .attempts()
        .into_iter()
        .map(|attempt| ProfuseGwTestAttempt {
            count: attempt.count,
            request_id: attempt.request_id,
            call_id: attempt.call_id,
            user_id: attempt.user_id,
            trace_id: attempt.trace_id,
            rpc_id: attempt.rpc_id,
            zone: attempt.zone,
            idc: attempt.idc,
            env: attempt.env,
            function: attempt.function,
            deadline_unix_ms: attempt.deadline_unix_ms,
        })
        .collect();
    ProfuseGwTestExecution { response, attempts }
}