Skip to main content

pcs_external/
builder.rs

1//! Builder for [`crate::PcsExternalClient`].
2
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use ppoppo_sdk_core::grpc::{Error, PathPrefixChannel};
7use ppoppo_sdk_core::token_cache::{
8    ClientCredentialsSource, TokenCache, TokenCacheConfig,
9};
10
11use crate::client::PcsExternalClient;
12use crate::scopes::PcsExternalScopeSet;
13
14/// Builder for [`PcsExternalClient<S>`].
15///
16/// ## Minimal wiring
17///
18/// ```no_run
19/// # async fn example() -> Result<(), pcs_external::Error> {
20/// use pcs_external::{PcsExternalClientBuilder, scopes::SendOnly};
21///
22/// let client = PcsExternalClientBuilder::new(
23///     "https://api.ppoppo.com/ext",
24///     "https://accounts.ppoppo.com/oauth/token",
25///     "my-client-id",
26///     "my-client-secret",
27/// )
28/// .build::<SendOnly>()
29/// .await?;
30/// # Ok(())
31/// # }
32/// ```
33///
34/// ## From environment variables
35///
36/// Reads `PCS_API_URL`, `PAS_TOKEN_URL`, `PAS_PCS_CLIENT_ID`,
37/// `PAS_PCS_CLIENT_SECRET`. Returns `None` if any is unset.
38///
39/// ```no_run
40/// # async fn example() -> Option<Result<(), pcs_external::Error>> {
41/// use pcs_external::{PcsExternalClientBuilder, scopes::SendOnly};
42///
43/// let client = PcsExternalClientBuilder::from_env()?
44///     .build::<SendOnly>()
45///     .await
46///     .ok()?;
47/// # Some(Ok(()))
48/// # }
49/// ```
50pub struct PcsExternalClientBuilder {
51    api_url: String,
52    token_url: String,
53    client_id: String,
54    client_secret: String,
55    cache_config: TokenCacheConfig,
56}
57
58impl PcsExternalClientBuilder {
59    /// Construct from explicit parameters.
60    pub fn new(
61        api_url: impl Into<String>,
62        token_url: impl Into<String>,
63        client_id: impl Into<String>,
64        client_secret: impl Into<String>,
65    ) -> Self {
66        Self {
67            api_url: api_url.into(),
68            token_url: token_url.into(),
69            client_id: client_id.into(),
70            client_secret: client_secret.into(),
71            cache_config: TokenCacheConfig::default(),
72        }
73    }
74
75    /// Construct from environment variables.
76    ///
77    /// Reads:
78    /// - `PCS_API_URL` — e.g. `https://api.ppoppo.com/ext`
79    /// - `PAS_TOKEN_URL` — e.g. `https://accounts.ppoppo.com/oauth/token`
80    /// - `PAS_PCS_CLIENT_ID` — OAuth2 client_id
81    /// - `PAS_PCS_CLIENT_SECRET` — OAuth2 client_secret
82    ///
83    /// Returns `None` if any variable is missing.
84    #[must_use]
85    pub fn from_env() -> Option<Self> {
86        let api_url = std::env::var("PCS_API_URL").ok()?;
87        let token_url = std::env::var("PAS_TOKEN_URL").ok()?;
88        let client_id = std::env::var("PAS_PCS_CLIENT_ID").ok()?;
89        let client_secret = std::env::var("PAS_PCS_CLIENT_SECRET").ok()?;
90        Some(Self::new(api_url, token_url, client_id, client_secret))
91    }
92
93    /// Override the token cache configuration (e.g. `refresh_skew`).
94    #[must_use]
95    pub fn with_cache_config(mut self, config: TokenCacheConfig) -> Self {
96        self.cache_config = config;
97        self
98    }
99
100    /// Build the client, connecting to the PCS External API endpoint.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`Error::Transport`] or [`Error::InvalidPathPrefix`] if the
105    /// connection cannot be established.
106    pub async fn build<S: PcsExternalScopeSet>(self) -> Result<PcsExternalClient<S>, Error> {
107        let channel = PathPrefixChannel::connect(&self.api_url).await?;
108        let source = ClientCredentialsSource::new(
109            self.token_url,
110            self.client_id,
111            self.client_secret,
112        );
113        let cache = Arc::new(TokenCache::new(Box::new(source), self.cache_config));
114        Ok(PcsExternalClient {
115            channel,
116            cache,
117            _scope: PhantomData,
118        })
119    }
120}