pcs-external 0.3.0

Ppoppo Chat System (PCS) External API client -- gRPC client for the External Developer Platform
Documentation
//! Builder for [`crate::PcsExternalClient`].

use std::marker::PhantomData;
use std::sync::Arc;

use ppoppo_sdk_core::token_cache::{
    ClientCredentialsSource, TokenCache, TokenCacheConfig,
};

use crate::client::PcsExternalClient;
use crate::error::Error;
use crate::scopes::PcsExternalScopeSet;
use crate::transport::ExternalChannel;

/// Builder for [`PcsExternalClient<S>`].
///
/// ## Minimal wiring
///
/// ```no_run
/// # async fn example() -> Result<(), pcs_external::Error> {
/// use pcs_external::{PcsExternalClientBuilder, scopes::SendOnly};
///
/// let client = PcsExternalClientBuilder::new(
///     "https://api.ppoppo.com/ext",
///     "https://accounts.ppoppo.com/oauth/token",
///     "my-client-id",
///     "my-client-secret",
/// )
/// .build::<SendOnly>()
/// .await?;
/// # Ok(())
/// # }
/// ```
///
/// ## From environment variables
///
/// Reads `PCS_API_URL`, `PAS_TOKEN_URL`, `PAS_PCS_CLIENT_ID`,
/// `PAS_PCS_CLIENT_SECRET`. Returns `None` if any is unset.
///
/// ```no_run
/// # async fn example() -> Option<Result<(), pcs_external::Error>> {
/// use pcs_external::{PcsExternalClientBuilder, scopes::SendOnly};
///
/// let client = PcsExternalClientBuilder::from_env()?
///     .build::<SendOnly>()
///     .await
///     .ok()?;
/// # Some(Ok(()))
/// # }
/// ```
pub struct PcsExternalClientBuilder {
    api_url: String,
    token_url: String,
    client_id: String,
    client_secret: String,
    cache_config: TokenCacheConfig,
}

impl PcsExternalClientBuilder {
    /// Construct from explicit parameters.
    pub fn new(
        api_url: impl Into<String>,
        token_url: impl Into<String>,
        client_id: impl Into<String>,
        client_secret: impl Into<String>,
    ) -> Self {
        Self {
            api_url: api_url.into(),
            token_url: token_url.into(),
            client_id: client_id.into(),
            client_secret: client_secret.into(),
            cache_config: TokenCacheConfig::default(),
        }
    }

    /// Construct from environment variables.
    ///
    /// Reads:
    /// - `PCS_API_URL` — e.g. `https://api.ppoppo.com/ext`
    /// - `PAS_TOKEN_URL` — e.g. `https://accounts.ppoppo.com/oauth/token`
    /// - `PAS_PCS_CLIENT_ID` — OAuth2 client_id
    /// - `PAS_PCS_CLIENT_SECRET` — OAuth2 client_secret
    ///
    /// Returns `None` if any variable is missing.
    #[must_use]
    pub fn from_env() -> Option<Self> {
        let api_url = std::env::var("PCS_API_URL").ok()?;
        let token_url = std::env::var("PAS_TOKEN_URL").ok()?;
        let client_id = std::env::var("PAS_PCS_CLIENT_ID").ok()?;
        let client_secret = std::env::var("PAS_PCS_CLIENT_SECRET").ok()?;
        Some(Self::new(api_url, token_url, client_id, client_secret))
    }

    /// Override the token cache configuration (e.g. `refresh_skew`).
    #[must_use]
    pub fn with_cache_config(mut self, config: TokenCacheConfig) -> Self {
        self.cache_config = config;
        self
    }

    /// Build the client, connecting to the PCS External API endpoint.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Transport`] or [`Error::InvalidPathPrefix`] if the
    /// connection cannot be established.
    pub async fn build<S: PcsExternalScopeSet>(self) -> Result<PcsExternalClient<S>, Error> {
        let channel = ExternalChannel::connect(&self.api_url).await?;
        let source = ClientCredentialsSource::new(
            self.token_url,
            self.client_id,
            self.client_secret,
        );
        let cache = Arc::new(TokenCache::new(Box::new(source), self.cache_config));
        Ok(PcsExternalClient {
            channel,
            cache,
            _scope: PhantomData,
        })
    }
}