ferriskey_sdk/config.rs
1//! SDK configuration primitives.
2//!
3//! ## Design Philosophy
4//!
5//! Configuration uses a typed builder pattern with compile-time validation.
6//! The `SdkConfigBuilder` uses TypeState markers to ensure that required
7//! fields (like `base_url`) are set before calling `.build()`.
8//!
9//! ## Example
10//!
11//! ```
12//! use std::time::Duration;
13//!
14//! use ferriskey_sdk::{AuthStrategy, SdkConfig};
15//!
16//! let config = SdkConfig::builder("https://api.example.com")
17//! .auth(AuthStrategy::Bearer("token".into()))
18//! .timeout(Duration::from_secs(30))
19//! .build();
20//! ```
21
22use std::time::Duration;
23
24// ---------------------------------------------------------------------------
25// Authentication Strategy
26// ---------------------------------------------------------------------------
27
28/// Authentication strategy applied to outgoing SDK requests.
29///
30/// ## Type Safety
31///
32/// Using an enum rather than `Option<String>` ensures that authentication
33/// modes are explicitly named and exhaustive pattern matching is possible.
34#[derive(Clone, Debug, Default, Eq, PartialEq)]
35pub enum AuthStrategy {
36 /// Do not attach authentication.
37 #[default]
38 None,
39 /// Attach an HTTP bearer token.
40 Bearer(String),
41 // Future variants can be added without breaking changes:
42 // ApiKey { header: String, key: String },
43 // OAuth2 { token: String, refresh: String },
44}
45
46impl AuthStrategy {
47 /// Returns `true` if authentication is configured.
48 #[must_use]
49 pub const fn is_configured(&self) -> bool {
50 !matches!(self, Self::None)
51 }
52
53 /// Returns the bearer token if this strategy is `Bearer`.
54 #[must_use]
55 pub const fn bearer_token(&self) -> Option<&String> {
56 match self {
57 Self::Bearer(token) => Some(token),
58 Self::None => None,
59 }
60 }
61}
62
63// ---------------------------------------------------------------------------
64// SdkConfig with Typed Builder
65// ---------------------------------------------------------------------------
66
67/// Runtime configuration for the FerrisKey SDK.
68///
69/// ## Immutability
70///
71/// Once built, `SdkConfig` is immutable. This prevents accidental mutation
72/// of configuration during request processing and enables safe sharing
73/// across async tasks without locks.
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct SdkConfig {
76 /// Base URL used to resolve relative request paths.
77 base_url: String,
78 /// Authentication strategy used for outgoing requests.
79 auth: AuthStrategy,
80 /// Request timeout duration.
81 timeout: Option<Duration>,
82 /// Custom user-agent header value.
83 user_agent: Option<String>,
84}
85
86impl SdkConfig {
87 /// Create a new SDK configuration.
88 ///
89 /// Prefer using [`Self::builder()`] for more complex configurations.
90 #[must_use]
91 pub fn new(base_url: impl Into<String>, auth: AuthStrategy) -> Self {
92 Self { base_url: base_url.into(), auth, timeout: None, user_agent: None }
93 }
94
95 /// Create a typed builder with the required base URL.
96 ///
97 /// The builder ensures the base URL is always provided.
98 #[must_use]
99 pub fn builder(base_url: impl Into<String>) -> SdkConfigBuilder<BaseUrlSet> {
100 SdkConfigBuilder {
101 base_url: base_url.into(),
102 auth: AuthStrategy::default(),
103 timeout: None,
104 user_agent: None,
105 _state: std::marker::PhantomData,
106 }
107 }
108
109 /// Access the base URL.
110 #[must_use]
111 pub fn base_url(&self) -> &str {
112 &self.base_url
113 }
114
115 /// Access the authentication strategy.
116 #[must_use]
117 pub const fn auth(&self) -> &AuthStrategy {
118 &self.auth
119 }
120
121 /// Access the configured timeout.
122 #[must_use]
123 pub const fn timeout(&self) -> Option<Duration> {
124 self.timeout
125 }
126
127 /// Access the user-agent string.
128 #[must_use]
129 pub fn user_agent(&self) -> Option<&str> {
130 self.user_agent.as_deref()
131 }
132}
133
134// ---------------------------------------------------------------------------
135// TypeState marker for SdkConfigBuilder
136// ---------------------------------------------------------------------------
137
138/// TypeState marker: base URL has been set.
139#[derive(Debug, Clone, Copy)]
140pub struct BaseUrlSet;
141
142/// Typed builder for [`SdkConfig`] with compile-time validation.
143///
144/// ## Type-State Pattern
145///
146/// The builder uses a phantom type parameter to track whether the required
147/// `base_url` field has been set. Calling `.build()` on an incomplete
148/// builder is a compile-time error.
149///
150/// ```compile_fail
151/// // This will not compile - base_url not set:
152/// let config = SdkConfigBuilder::<()>::new().build();
153/// ```
154#[derive(Debug)]
155pub struct SdkConfigBuilder<S> {
156 base_url: String,
157 auth: AuthStrategy,
158 timeout: Option<Duration>,
159 user_agent: Option<String>,
160 _state: std::marker::PhantomData<S>,
161}
162
163impl SdkConfigBuilder<BaseUrlSet> {
164 /// Build the configuration. Available only when base_url is set.
165 #[must_use]
166 pub fn build(self) -> SdkConfig {
167 SdkConfig {
168 base_url: self.base_url,
169 auth: self.auth,
170 timeout: self.timeout,
171 user_agent: self.user_agent,
172 }
173 }
174}
175
176impl<S> SdkConfigBuilder<S> {
177 /// Set the authentication strategy.
178 #[must_use]
179 pub fn auth(mut self, auth: AuthStrategy) -> Self {
180 self.auth = auth;
181 self
182 }
183
184 /// Set the request timeout.
185 #[must_use]
186 pub const fn timeout(mut self, timeout: Duration) -> Self {
187 self.timeout = Some(timeout);
188 self
189 }
190
191 /// Set the user-agent header.
192 #[must_use]
193 pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
194 self.user_agent = Some(agent.into());
195 self
196 }
197}
198
199// ---------------------------------------------------------------------------
200// Extension trait for fluent AuthStrategy construction
201// ---------------------------------------------------------------------------
202
203/// Extension trait for building auth strategies fluently.
204pub trait AuthStrategyExt {
205 /// Create a bearer auth strategy.
206 fn bearer(token: impl Into<String>) -> AuthStrategy;
207}
208
209impl AuthStrategyExt for AuthStrategy {
210 fn bearer(token: impl Into<String>) -> Self {
211 Self::Bearer(token.into())
212 }
213}