pcs-external 0.2.0

Ppoppo Chat System (PCS) External API client -- gRPC client for the External Developer Platform
Documentation
//! `PcsExternalPort` — the network boundary at PCS External API.
//!
//! Two first-class methods covering the hot path
//! (`ExternalMessageService::CreateSendRequest` and
//! `GetSendRequestStatus`) plus a deliberately-unergonomic escape hatch
//! returning the underlying `tonic` channel for templates / admin /
//! polls / observability — RPCs that consumers reach for rarely.
//!
//! Adapters translate transport-level errors (tonic::Status, body decode
//! failures) into [`super::PcsFailure`] at this boundary; downstream SDK
//! and consumer code is gRPC-free on the hot path.
//!
//! Production adapter: [`super::GrpcPcsAdapter`].
//! Test adapter: [`super::MemoryPcsExternal`] (gated behind the
//! `test-support` Cargo feature).

use std::future::Future;

use super::failure::PcsFailure;
use super::types::{PollConfig, RecipientList, SendOutcome, SendRequestId, SendStatus, TemplateId};
use crate::external::ExternalChannel;

/// Minimum surface SDK consumers need to send templated batch alerts
/// and poll their delivery status.
///
/// All hot-path methods are domain-typed — proto types do not appear in
/// these signatures. The escape hatch [`Self::raw_channel`] is the sole
/// place `tonic` types resurface, and is intentionally awkward to use
/// so cost-driven graduation moves common RPCs into first-class methods
/// rather than entrenching the escape hatch.
pub trait PcsExternalPort: Send + Sync + 'static {
    /// Hot path: send a batch of templated messages.
    ///
    /// Translates to `ExternalMessageService::CreateSendRequest` on the
    /// gRPC adapter. The cited [`TemplateId`] must already exist on the
    /// PCS External platform (registered out-of-band via
    /// `ExternalTemplateService::CreateTemplate`).
    ///
    /// # Errors
    ///
    /// Returns [`PcsFailure::Rejected`] for application-level rejections
    /// (template not found, recipient quota exceeded, malformed
    /// arguments), [`PcsFailure::ServerError`] for retry-eligible 5xx
    /// states, [`PcsFailure::Transport`] for network-class faults.
    fn send_alert(
        &self,
        template: &TemplateId,
        recipients: &RecipientList,
        poll: Option<&PollConfig>,
    ) -> impl Future<Output = Result<SendOutcome, PcsFailure>> + Send;

    /// Hot path: poll aggregate delivery status for a previously-issued
    /// send request.
    ///
    /// Translates to `ExternalMessageService::GetSendRequestStatus`.
    /// Returns aggregate counters only — per-recipient detail is reachable
    /// via the [`Self::raw_channel`] escape hatch when needed.
    ///
    /// # Errors
    ///
    /// Same shape as [`Self::send_alert`].
    fn get_send_status(
        &self,
        id: &SendRequestId,
    ) -> impl Future<Output = Result<SendStatus, PcsFailure>> + Send;

    /// Escape hatch for templates / admin / polls / observability — RPCs
    /// outside the hot path.
    ///
    /// Returns the underlying [`ExternalChannel`] plus the bound API key.
    /// Caller instantiates whichever `tonic` service-client struct they
    /// need and uses [`crate::external::auth_request`] to wrap each
    /// outgoing body with `Authorization: Bearer <api_key>`.
    ///
    /// **This API is intentionally unergonomic.** When an "advanced" RPC
    /// graduates to common usage, prefer adding a first-class method
    /// here (and bumping a minor version) over entrenching escape-hatch
    /// call sites.
    fn raw_channel(&self) -> RawPcsChannel<'_>;
}

/// Escape-hatch handle for `tonic` service clients beyond the hot path.
///
/// Lifetime borrow of the adapter's API key keeps the secret from being
/// duplicated into per-RPC structs while still allowing a caller to pass
/// it through [`crate::external::auth_request`] on each request.
#[derive(Debug)]
pub struct RawPcsChannel<'a> {
    /// Path-prefix-aware tonic channel; clones cheaply (`Arc<Channel>`).
    pub channel: ExternalChannel,
    /// Validated `pk_live_…` / `pk_test_…` API key. Already passed
    /// header-value validation via the adapter's constructor.
    pub api_key: &'a str,
}