1use crate::error::{Error, Result};
2
3const PROFILE_NAME_ENTERPRISE: &str = "enterprise";
4const PROFILE_NAME_PERSONAL: &str = "personal";
5const PROFILE_NAME_WORKSPACE: &str = "workspace";
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ApiProfile {
10 Enterprise,
11 Personal,
12 Workspace,
13}
14
15impl ApiProfile {
16 pub fn as_str(&self) -> &'static str {
17 match self {
18 ApiProfile::Enterprise => PROFILE_NAME_ENTERPRISE,
19 ApiProfile::Personal => PROFILE_NAME_PERSONAL,
20 ApiProfile::Workspace => PROFILE_NAME_WORKSPACE,
21 }
22 }
23
24 pub fn parse(input: &str) -> Result<Self> {
25 match input.trim().to_ascii_lowercase().as_str() {
26 PROFILE_NAME_ENTERPRISE => Ok(ApiProfile::Enterprise),
27 PROFILE_NAME_PERSONAL => Ok(ApiProfile::Personal),
28 PROFILE_NAME_WORKSPACE => Ok(ApiProfile::Workspace),
29 other => Err(Error::Endpoint(format!("unsupported API profile: {other}"))),
30 }
31 }
32
33 pub fn requires_experimental_flag(&self) -> bool {
34 matches!(self, ApiProfile::Personal | ApiProfile::Workspace)
35 }
36}
37
38pub const PROFILE_EXPERIMENT_FLAG: &str = "NBLM_PROFILE_EXPERIMENT";
39
40#[derive(Debug, Clone)]
41pub enum ProfileParams {
42 Enterprise {
43 project_number: String,
44 location: String,
45 endpoint_location: String,
46 },
47 Personal {
48 user_email: Option<String>,
49 },
50 Workspace {
51 customer_id: Option<String>,
52 admin_email: Option<String>,
53 },
54}
55
56impl ProfileParams {
57 pub fn enterprise(
58 project_number: impl Into<String>,
59 location: impl Into<String>,
60 endpoint_location: impl Into<String>,
61 ) -> Self {
62 Self::Enterprise {
63 project_number: project_number.into(),
64 location: location.into(),
65 endpoint_location: endpoint_location.into(),
66 }
67 }
68
69 pub fn personal<T: Into<String>>(user_email: Option<T>) -> Self {
70 Self::Personal {
71 user_email: user_email.map(|email| email.into()),
72 }
73 }
74
75 pub fn workspace<T: Into<String>, U: Into<String>>(
76 customer_id: Option<T>,
77 admin_email: Option<U>,
78 ) -> Self {
79 Self::Workspace {
80 customer_id: customer_id.map(|value| value.into()),
81 admin_email: admin_email.map(|value| value.into()),
82 }
83 }
84
85 pub fn expected_profile(&self) -> ApiProfile {
86 match self {
87 ProfileParams::Enterprise { .. } => ApiProfile::Enterprise,
88 ProfileParams::Personal { .. } => ApiProfile::Personal,
89 ProfileParams::Workspace { .. } => ApiProfile::Workspace,
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct EnvironmentConfig {
97 profile: ApiProfile,
98 base_url: String,
99 parent_path: String,
100}
101
102impl EnvironmentConfig {
103 pub fn enterprise(
105 project_number: impl Into<String>,
106 location: impl Into<String>,
107 endpoint_location: impl Into<String>,
108 ) -> Result<Self> {
109 Self::from_profile(
110 ApiProfile::Enterprise,
111 ProfileParams::enterprise(project_number, location, endpoint_location),
112 )
113 }
114
115 pub fn profile(&self) -> ApiProfile {
116 self.profile
117 }
118
119 pub fn base_url(&self) -> &str {
120 &self.base_url
121 }
122
123 pub fn parent_path(&self) -> &str {
124 &self.parent_path
125 }
126
127 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
129 self.base_url = base_url.into();
130 self
131 }
132
133 pub fn from_profile(profile: ApiProfile, params: ProfileParams) -> Result<Self> {
134 let params_profile = params.expected_profile();
135 if profile != params_profile {
136 return Err(profile_params_mismatch_error(profile, params_profile));
137 }
138
139 match profile {
140 ApiProfile::Enterprise => match params {
141 ProfileParams::Enterprise {
142 project_number,
143 location,
144 endpoint_location,
145 } => {
146 let endpoint = normalize_endpoint_location(endpoint_location)?;
147 let base_url =
148 format!("https://{}discoveryengine.googleapis.com/v1alpha", endpoint);
149 let parent_path = format!("projects/{}/locations/{}", project_number, location);
150 Ok(Self {
151 profile: ApiProfile::Enterprise,
152 base_url,
153 parent_path,
154 })
155 }
156 _ => unreachable!("profile/params mismatch should already be validated"),
157 },
158 ApiProfile::Personal | ApiProfile::Workspace => Err(unsupported_profile_error(profile)),
159 }
160 }
161}
162
163pub fn normalize_endpoint_location(input: String) -> Result<String> {
165 let trimmed = input.trim().trim_end_matches('-').to_lowercase();
166 let normalized = match trimmed.as_str() {
167 "us" => "us-",
168 "eu" => "eu-",
169 "global" => "global-",
170 other => {
171 return Err(Error::Endpoint(format!(
172 "unsupported endpoint location: {other}"
173 )))
174 }
175 };
176 Ok(normalized.to_string())
177}
178
179fn unsupported_profile_error(profile: ApiProfile) -> Error {
180 Error::Endpoint(format!(
181 "API profile '{}' is not available yet",
182 profile.as_str()
183 ))
184}
185
186fn profile_params_mismatch_error(expected: ApiProfile, provided: ApiProfile) -> Error {
187 Error::Endpoint(format!(
188 "profile '{}' expects parameters for '{}', but '{}' parameters were provided",
189 expected.as_str(),
190 expected.as_str(),
191 provided.as_str()
192 ))
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn enterprise_constructor_builds_expected_urls() {
201 let env = EnvironmentConfig::enterprise("123", "global", "us").unwrap();
202 assert_eq!(env.profile(), ApiProfile::Enterprise);
203 assert_eq!(
204 env.base_url(),
205 "https://us-discoveryengine.googleapis.com/v1alpha"
206 );
207 assert_eq!(env.parent_path(), "projects/123/locations/global");
208 }
209
210 #[test]
211 fn normalize_endpoint_location_variants() {
212 assert_eq!(
213 normalize_endpoint_location("us".into()).unwrap(),
214 "us-".to_string()
215 );
216 assert_eq!(
217 normalize_endpoint_location("eu-".into()).unwrap(),
218 "eu-".to_string()
219 );
220 assert_eq!(
221 normalize_endpoint_location(" global ".into()).unwrap(),
222 "global-".to_string()
223 );
224 }
225
226 #[test]
227 fn normalize_endpoint_location_invalid() {
228 let err = normalize_endpoint_location("asia".into()).unwrap_err();
229 assert!(format!("{err}").contains("unsupported endpoint location"));
230 }
231
232 #[test]
233 fn with_base_url_overrides_base_url() {
234 let env = EnvironmentConfig::enterprise("123", "global", "us")
235 .unwrap()
236 .with_base_url("http://localhost:8080/v1alpha");
237 assert_eq!(env.base_url(), "http://localhost:8080/v1alpha");
238 assert_eq!(env.parent_path(), "projects/123/locations/global");
239 }
240
241 #[test]
242 fn api_profile_parse_accepts_all_known_variants() {
243 let enterprise = ApiProfile::parse("enterprise").unwrap();
244 assert_eq!(enterprise, ApiProfile::Enterprise);
245 assert_eq!(enterprise.as_str(), "enterprise");
246
247 let personal = ApiProfile::parse("personal").unwrap();
248 assert_eq!(personal, ApiProfile::Personal);
249 assert_eq!(personal.as_str(), "personal");
250
251 let workspace = ApiProfile::parse("workspace").unwrap();
252 assert_eq!(workspace, ApiProfile::Workspace);
253 assert_eq!(workspace.as_str(), "workspace");
254 }
255
256 #[test]
257 fn from_profile_rejects_mismatched_params() {
258 let err = EnvironmentConfig::from_profile(
259 ApiProfile::Enterprise,
260 ProfileParams::personal::<String>(None),
261 )
262 .unwrap_err();
263 let msg = format!("{err}");
264 assert!(msg.contains("expects parameters for 'enterprise'"));
265 }
266
267 #[test]
268 fn personal_profile_not_available_yet() {
269 let err = EnvironmentConfig::from_profile(
270 ApiProfile::Personal,
271 ProfileParams::personal(Some("user@example.com")),
272 )
273 .unwrap_err();
274 let msg = format!("{err}");
275 assert!(msg.contains("not available yet"));
276 }
277
278 #[test]
279 fn workspace_profile_not_available_yet() {
280 let err = EnvironmentConfig::from_profile(
281 ApiProfile::Workspace,
282 ProfileParams::workspace::<String, String>(None, None),
283 )
284 .unwrap_err();
285 let msg = format!("{err}");
286 assert!(msg.contains("not available yet"));
287 }
288
289 #[test]
290 fn profile_params_expected_profile_returns_correct_variant() {
291 let enterprise = ProfileParams::enterprise("123", "global", "us");
292 assert_eq!(enterprise.expected_profile(), ApiProfile::Enterprise);
293
294 let personal = ProfileParams::personal(Some("user@example.com"));
295 assert_eq!(personal.expected_profile(), ApiProfile::Personal);
296
297 let workspace = ProfileParams::workspace(Some("customer123"), Some("admin@example.com"));
298 assert_eq!(workspace.expected_profile(), ApiProfile::Workspace);
299 }
300
301 #[test]
302 fn from_profile_succeeds_with_matching_enterprise_params() {
303 let env = EnvironmentConfig::from_profile(
304 ApiProfile::Enterprise,
305 ProfileParams::enterprise("456", "us", "us"),
306 )
307 .unwrap();
308 assert_eq!(env.profile(), ApiProfile::Enterprise);
309 assert_eq!(
310 env.base_url(),
311 "https://us-discoveryengine.googleapis.com/v1alpha"
312 );
313 assert_eq!(env.parent_path(), "projects/456/locations/us");
314 }
315
316 #[test]
317 fn from_profile_rejects_personal_profile_with_enterprise_params() {
318 let err = EnvironmentConfig::from_profile(
319 ApiProfile::Personal,
320 ProfileParams::enterprise("123", "global", "us"),
321 )
322 .unwrap_err();
323 let msg = format!("{err}");
324 assert!(msg.contains("expects parameters for 'personal'"));
325 assert!(msg.contains("'enterprise' parameters were provided"));
326 }
327
328 #[test]
329 fn from_profile_rejects_workspace_profile_with_enterprise_params() {
330 let err = EnvironmentConfig::from_profile(
331 ApiProfile::Workspace,
332 ProfileParams::enterprise("123", "global", "us"),
333 )
334 .unwrap_err();
335 let msg = format!("{err}");
336 assert!(msg.contains("expects parameters for 'workspace'"));
337 assert!(msg.contains("'enterprise' parameters were provided"));
338 }
339
340 #[test]
341 fn profile_params_personal_builder_handles_optional_email() {
342 let with_email = ProfileParams::personal(Some("user@example.com"));
343 assert_eq!(with_email.expected_profile(), ApiProfile::Personal);
344
345 let without_email = ProfileParams::personal::<String>(None);
346 assert_eq!(without_email.expected_profile(), ApiProfile::Personal);
347 }
348
349 #[test]
350 fn profile_params_workspace_builder_handles_optional_fields() {
351 let with_both = ProfileParams::workspace(Some("customer123"), Some("admin@example.com"));
352 assert_eq!(with_both.expected_profile(), ApiProfile::Workspace);
353
354 let with_customer_only = ProfileParams::workspace(Some("customer123"), None::<String>);
355 assert_eq!(with_customer_only.expected_profile(), ApiProfile::Workspace);
356
357 let with_admin_only = ProfileParams::workspace(None::<String>, Some("admin@example.com"));
358 assert_eq!(with_admin_only.expected_profile(), ApiProfile::Workspace);
359
360 let with_neither = ProfileParams::workspace::<String, String>(None, None);
361 assert_eq!(with_neither.expected_profile(), ApiProfile::Workspace);
362 }
363
364 #[test]
365 fn requires_experimental_flag_returns_correct_values() {
366 assert!(!ApiProfile::Enterprise.requires_experimental_flag());
367 assert!(ApiProfile::Personal.requires_experimental_flag());
368 assert!(ApiProfile::Workspace.requires_experimental_flag());
369 }
370}