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