Skip to main content

sdk_rust/
builder.rs

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}