1use crate::endpoints::{EndpointManager, DEFAULT_HOST};
2use derive_builder::Builder;
3use tracing::warn;
4
5#[cfg(not(feature = "async-client"))]
6mod blocking;
7#[cfg(not(feature = "async-client"))]
8pub use blocking::client;
9#[cfg(not(feature = "async-client"))]
10pub use blocking::Client;
11
12#[cfg(feature = "async-client")]
13mod async_client;
14#[cfg(feature = "async-client")]
15pub use async_client::client;
16#[cfg(feature = "async-client")]
17pub use async_client::Client;
18
19#[derive(Builder, Clone)]
36pub struct ClientOptions {
37 #[builder(setter(into, strip_option), default)]
39 host: Option<String>,
40
41 api_key: String,
43
44 #[builder(default = "30")]
46 request_timeout_seconds: u64,
47
48 #[builder(setter(into, strip_option), default)]
50 personal_api_key: Option<String>,
51
52 #[builder(default = "false")]
54 enable_local_evaluation: bool,
55
56 #[builder(default = "30")]
58 poll_interval_seconds: u64,
59
60 #[builder(default = "false")]
62 disabled: bool,
63
64 #[builder(default = "false")]
66 disable_geoip: bool,
67
68 #[builder(default = "3")]
70 feature_flags_request_timeout_seconds: u64,
71
72 #[builder(default = "false")]
77 local_evaluation_only: bool,
78
79 #[builder(setter(skip))]
80 #[builder(default = "EndpointManager::new(DEFAULT_HOST.to_string())")]
81 endpoint_manager: EndpointManager,
82}
83
84impl ClientOptions {
85 pub(crate) fn endpoints(&self) -> &EndpointManager {
87 &self.endpoint_manager
88 }
89
90 pub fn is_disabled(&self) -> bool {
92 self.disabled
93 }
94
95 fn sanitize(mut self) -> Self {
96 self.api_key = self.api_key.trim().to_string();
97 if self.api_key.is_empty() {
98 warn!("api_key is empty after trimming whitespace; check your project API key");
99 }
100 self.host = Some(match self.host {
101 Some(host) => {
102 let normalized = host.trim().to_string();
103 if normalized.is_empty() {
104 DEFAULT_HOST.to_string()
105 } else {
106 normalized
107 }
108 }
109 None => DEFAULT_HOST.to_string(),
110 });
111 self.personal_api_key = self.personal_api_key.and_then(|personal_api_key| {
112 let normalized = personal_api_key.trim().to_string();
113 if normalized.is_empty() {
114 None
115 } else {
116 Some(normalized)
117 }
118 });
119 self.endpoint_manager = EndpointManager::new(
120 self.host
121 .clone()
122 .expect("host is always normalized in sanitize"),
123 );
124 self
125 }
126
127 fn with_endpoint_manager(self) -> Self {
129 self.sanitize()
130 }
131}
132
133impl From<&str> for ClientOptions {
134 fn from(api_key: &str) -> Self {
135 ClientOptionsBuilder::default()
136 .api_key(api_key.to_string())
137 .build()
138 .expect("We always set the API key, so this is infallible")
139 .with_endpoint_manager()
140 }
141}
142
143impl From<(&str, &str)> for ClientOptions {
144 fn from((api_key, host): (&str, &str)) -> Self {
146 ClientOptionsBuilder::default()
147 .api_key(api_key.to_string())
148 .host(host.to_string())
149 .build()
150 .expect("We always set the API key, so this is infallible")
151 .with_endpoint_manager()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::ClientOptionsBuilder;
158 use crate::endpoints::{EU_INGESTION_ENDPOINT, US_INGESTION_ENDPOINT};
159
160 #[test]
161 fn trims_whitespace_sensitive_options() {
162 let options = ClientOptionsBuilder::default()
163 .api_key(" \n test-api-key\t ".to_string())
164 .host(" \nhttps://eu.posthog.com/\t ")
165 .personal_api_key(" \n\t ")
166 .build()
167 .unwrap()
168 .sanitize();
169
170 assert_eq!(options.api_key, "test-api-key");
171 assert_eq!(options.host.as_deref(), Some("https://eu.posthog.com/"));
172 assert_eq!(options.personal_api_key, None);
173 assert_eq!(options.endpoints().api_host(), EU_INGESTION_ENDPOINT);
174 }
175
176 #[test]
177 fn defaults_blank_host_after_trimming_whitespace() {
178 let options = ClientOptionsBuilder::default()
179 .api_key("test-api-key".to_string())
180 .host(" \n\t ")
181 .build()
182 .unwrap()
183 .sanitize();
184
185 assert_eq!(options.host.as_deref(), Some(US_INGESTION_ENDPOINT));
186 assert_eq!(options.endpoints().api_host(), US_INGESTION_ENDPOINT);
187 }
188}