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}