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}