1use std::{collections::BTreeMap, time::Duration};
2
3use crate::{
4 client::{Client, ClientAuthStrategy, SdkClientCredentialsAuth},
5 error::SdkError,
6};
7
8pub struct ClientBuilder {
9 base_url: String,
10 bearer_token: Option<String>,
11 client_id: Option<String>,
12 client_secret: Option<String>,
13 tenant_id: Option<String>,
14 user_id: Option<String>,
15 timeout_secs: Option<u64>,
16 token_exchange_path: Option<String>,
17 requested_scopes: Vec<String>,
18 headers: BTreeMap<String, String>,
19}
20
21impl ClientBuilder {
22 pub fn new(base_url: impl Into<String>) -> Self {
23 Self {
24 base_url: base_url.into(),
25 bearer_token: None,
26 client_id: None,
27 client_secret: None,
28 tenant_id: None,
29 user_id: None,
30 timeout_secs: None,
31 token_exchange_path: None,
32 requested_scopes: Vec::new(),
33 headers: BTreeMap::new(),
34 }
35 }
36
37 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
38 self.bearer_token = Some(bearer_token.into());
39 self
40 }
41
42 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
43 self.client_id = Some(client_id.into());
44 self
45 }
46
47 pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
48 self.client_secret = Some(client_secret.into());
49 self
50 }
51
52 pub fn with_token_exchange_path(mut self, token_exchange_path: impl Into<String>) -> Self {
53 self.token_exchange_path = Some(token_exchange_path.into());
54 self
55 }
56
57 pub fn with_requested_scopes<I, S>(mut self, scopes: I) -> Self
58 where
59 I: IntoIterator<Item = S>,
60 S: Into<String>,
61 {
62 self.requested_scopes = scopes
63 .into_iter()
64 .map(Into::into)
65 .map(|scope| scope.trim().to_string())
66 .filter(|scope| !scope.is_empty())
67 .collect();
68 self.requested_scopes.sort();
69 self.requested_scopes.dedup();
70 self
71 }
72
73 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
74 self.tenant_id = Some(tenant_id.into());
75 self
76 }
77
78 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
79 self.user_id = Some(user_id.into());
80 self
81 }
82
83 pub fn with_timeout_secs(mut self, timeout_secs: u64) -> Self {
84 self.timeout_secs = Some(timeout_secs);
85 self
86 }
87
88 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
89 self.headers.insert(name.into(), value.into());
90 self
91 }
92
93 pub fn build(self) -> Result<Client, SdkError> {
94 let base_url = self.base_url.trim().trim_end_matches('/').to_string();
95 if base_url.is_empty() {
96 return Err(SdkError::InvalidInput(
97 "base_url cannot be empty".to_string(),
98 ));
99 }
100
101 let mut headers = self.headers;
102 let tenant_id = self.tenant_id.map(|tenant_id| tenant_id.trim().to_string());
103 if let Some(tenant_id) = tenant_id.as_deref()
104 && !tenant_id.is_empty()
105 {
106 headers.insert("x-lattix-tenant-id".to_string(), tenant_id.to_string());
107 }
108 if let Some(user_id) = self.user_id {
109 let user_id = user_id.trim();
110 if !user_id.is_empty() {
111 headers.insert("x-lattix-user-id".to_string(), user_id.to_string());
112 }
113 }
114
115 let mut agent_builder = ureq::AgentBuilder::new();
116 if let Some(timeout_secs) = self.timeout_secs {
117 agent_builder = agent_builder.timeout(Duration::from_secs(timeout_secs));
118 }
119
120 let auth_strategy = if let Some(bearer_token) = self.bearer_token {
121 let bearer_token = bearer_token.trim().to_string();
122 if bearer_token.is_empty() {
123 None
124 } else {
125 Some(ClientAuthStrategy::StaticBearer(format!(
126 "Bearer {bearer_token}"
127 )))
128 }
129 } else {
130 match (self.client_id, self.client_secret) {
131 (Some(client_id), Some(client_secret)) => {
132 let tenant_id = tenant_id.ok_or_else(|| {
133 SdkError::InvalidInput(
134 "tenant_id is required when sdk client credentials are configured"
135 .to_string(),
136 )
137 })?;
138 let client_id = client_id.trim().to_string();
139 let client_secret = client_secret.trim().to_string();
140 if client_id.is_empty() || client_secret.is_empty() {
141 return Err(SdkError::InvalidInput(
142 "client_id and client_secret cannot be empty".to_string(),
143 ));
144 }
145
146 Some(ClientAuthStrategy::SdkClientCredentials(Box::new(
147 SdkClientCredentialsAuth::new(
148 tenant_id,
149 client_id,
150 client_secret,
151 self.token_exchange_path
152 .unwrap_or_else(|| "/v1/sdk/session".to_string()),
153 self.requested_scopes,
154 ),
155 )))
156 }
157 (None, None) => None,
158 _ => {
159 return Err(SdkError::InvalidInput(
160 "client_id and client_secret must be provided together".to_string(),
161 ));
162 }
163 }
164 };
165
166 Ok(Client::new(
167 base_url,
168 agent_builder.build(),
169 headers,
170 auth_strategy,
171 ))
172 }
173}