1use std::{sync::Arc, time::Duration};
2
3use reqwest::{Client, Url};
4
5use crate::auth::TokenProvider;
6use crate::error::Result;
7
8mod api;
9mod http;
10mod retry;
11mod url_builder;
12
13pub use self::retry::{RetryConfig, Retryer};
14
15use self::http::HttpClient;
16use self::url_builder::{normalize_endpoint_location, UrlBuilder};
17
18const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
19
20pub struct NblmClient {
21 pub(self) http: HttpClient,
22 pub(self) url_builder: UrlBuilder,
23 timeout: Duration,
24}
25
26impl NblmClient {
27 pub fn new(
28 token_provider: Arc<dyn TokenProvider>,
29 project_number: impl Into<String>,
30 location: impl Into<String>,
31 endpoint_location: impl Into<String>,
32 ) -> Result<Self> {
33 let project_number = project_number.into();
34 let location = location.into();
35 let endpoint_location = endpoint_location.into();
36 let base = format!(
37 "https://{}discoveryengine.googleapis.com/v1alpha",
38 normalize_endpoint_location(endpoint_location)?
39 );
40 let parent = format!("projects/{}/locations/{}", project_number, location);
41
42 let client = Client::builder()
43 .user_agent(concat!("nblm-cli/", env!("CARGO_PKG_VERSION")))
44 .timeout(DEFAULT_TIMEOUT)
45 .build()
46 .map_err(crate::error::Error::from)?;
47
48 let retryer = Retryer::new(RetryConfig::default());
49 let http = HttpClient::new(client, token_provider, retryer, None);
50 let url_builder = UrlBuilder::new(base, parent);
51
52 Ok(Self {
53 http,
54 url_builder,
55 timeout: DEFAULT_TIMEOUT,
56 })
57 }
58
59 pub fn with_timeout(mut self, timeout: Duration) -> Self {
60 self.timeout = timeout;
61 let client = Client::builder()
63 .user_agent(concat!("nblm-cli/", env!("CARGO_PKG_VERSION")))
64 .timeout(timeout)
65 .build()
66 .expect("Failed to rebuild client with new timeout");
67
68 let token_provider = Arc::clone(&self.http.token_provider);
69 let retryer = self.http.retryer.clone();
70 let user_project = self.http.user_project.clone();
71 self.http = HttpClient::new(client, token_provider, retryer, user_project);
72 self
73 }
74
75 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
76 let client = Client::builder()
77 .user_agent(concat!("nblm-cli/", env!("CARGO_PKG_VERSION")))
78 .timeout(self.timeout)
79 .build()
80 .expect("Failed to rebuild client");
81
82 let token_provider = Arc::clone(&self.http.token_provider);
83 let retryer = Retryer::new(config);
84 let user_project = self.http.user_project.clone();
85 self.http = HttpClient::new(client, token_provider, retryer, user_project);
86 self
87 }
88
89 pub fn with_user_project(mut self, project: impl Into<String>) -> Self {
90 let client = Client::builder()
91 .user_agent(concat!("nblm-cli/", env!("CARGO_PKG_VERSION")))
92 .timeout(self.timeout)
93 .build()
94 .expect("Failed to rebuild client");
95
96 let token_provider = Arc::clone(&self.http.token_provider);
97 let retryer = self.http.retryer.clone();
98 let user_project = Some(project.into());
99 self.http = HttpClient::new(client, token_provider, retryer, user_project);
100 self
101 }
102
103 pub fn with_base_url(mut self, base: impl Into<String>) -> Result<Self> {
105 let base = base.into().trim().trim_end_matches('/').to_string();
106 let _ = Url::parse(&base).map_err(crate::error::Error::from)?;
108 let parent = self.url_builder.parent.clone();
109 self.url_builder = UrlBuilder::new(base, parent);
110 Ok(self)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn with_base_url_accepts_absolute_url() {
120 let provider = Arc::new(crate::auth::StaticTokenProvider::new("test"));
121 let client = NblmClient::new(provider, "123", "global", "us").unwrap();
122 let result = client.with_base_url("http://localhost:8080/v1alpha");
123 assert!(result.is_ok());
124 }
125
126 #[test]
127 fn with_base_url_trims_trailing_slash() {
128 let provider = Arc::new(crate::auth::StaticTokenProvider::new("test"));
129 let client = NblmClient::new(provider, "123", "global", "us")
130 .unwrap()
131 .with_base_url("http://example.com/v1alpha/")
132 .unwrap();
133
134 let url = client.url_builder.build_url("/test").unwrap();
136 assert_eq!(url.as_str(), "http://example.com/v1alpha/test");
137 }
138
139 #[test]
140 fn with_base_url_rejects_relative_path() {
141 let provider = Arc::new(crate::auth::StaticTokenProvider::new("test"));
142 let client = NblmClient::new(provider, "123", "global", "us").unwrap();
143 let result = client.with_base_url("/relative/path");
144 assert!(result.is_err());
145 }
146}