1use std::fmt;
24use std::str::FromStr;
25
26use crate::config::LaminarConfig;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum Profile {
34 #[default]
36 BareMetal,
37 Embedded,
39 Durable,
41 Delta,
43}
44
45impl Profile {
46 #[must_use]
58 pub fn from_config(config: &LaminarConfig, has_discovery: bool) -> Self {
59 if has_discovery {
60 return Self::Delta;
61 }
62 if let Some(url) = &config.object_store_url {
63 if url.starts_with("s3://")
64 || url.starts_with("gs://")
65 || url.starts_with("az://")
66 || url.starts_with("abfs://")
67 {
68 return Self::Durable;
69 }
70 if url.starts_with("file://") {
71 return Self::Embedded;
72 }
73 }
74 if config.storage_dir.is_some() {
75 return Self::Embedded;
76 }
77 Self::BareMetal
78 }
79
80 pub fn validate_features(self) -> Result<(), ProfileError> {
89 match self {
95 Self::BareMetal | Self::Embedded | Self::Durable | Self::Delta => Ok(()),
96 }
97 }
98
99 pub fn validate_config(
108 self,
109 config: &LaminarConfig,
110 object_store_url: Option<&str>,
111 ) -> Result<(), ProfileError> {
112 match self {
113 Self::BareMetal => Ok(()),
114 Self::Embedded => {
115 if config.storage_dir.is_none() {
116 return Err(ProfileError::RequirementNotMet(
117 "Embedded profile requires a storage_dir".into(),
118 ));
119 }
120 Ok(())
121 }
122 Self::Durable | Self::Delta => {
123 if object_store_url.is_none() {
124 return Err(ProfileError::RequirementNotMet(
125 "Durable/Delta profile requires an \
126 object_store_url"
127 .into(),
128 ));
129 }
130 Ok(())
131 }
132 }
133 }
134
135 pub fn apply_defaults(self, config: &mut LaminarConfig) {
139 match self {
140 Self::BareMetal => {
141 }
143 Self::Embedded => {
144 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
146 config.default_buffer_size = 32_768;
147 }
148 }
149 Self::Durable => {
150 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
152 config.default_buffer_size = 131_072;
153 }
154 }
155 Self::Delta => {
156 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
158 config.default_buffer_size = 262_144;
159 }
160 }
161 }
162 }
163}
164
165impl FromStr for Profile {
166 type Err = ProfileError;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
169 match s.to_ascii_lowercase().as_str() {
170 "bare_metal" | "baremetal" | "bare-metal" => Ok(Self::BareMetal),
171 "embedded" => Ok(Self::Embedded),
172 "durable" => Ok(Self::Durable),
173 "delta" => Ok(Self::Delta),
174 _ => Err(ProfileError::UnknownProfileName(s.into())),
175 }
176 }
177}
178
179impl fmt::Display for Profile {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 Self::BareMetal => write!(f, "bare_metal"),
183 Self::Embedded => write!(f, "embedded"),
184 Self::Durable => write!(f, "durable"),
185 Self::Delta => write!(f, "delta"),
186 }
187 }
188}
189
190#[derive(Debug, thiserror::Error)]
192pub enum ProfileError {
193 #[error("profile requirement not met: {0}")]
195 RequirementNotMet(String),
196
197 #[error("feature `{0}` not compiled — enable it in Cargo.toml")]
199 FeatureNotCompiled(String),
200
201 #[error("unknown profile name: {0}")]
203 UnknownProfileName(String),
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_bare_metal_zero_config() {
212 let config = LaminarConfig::default();
213 let profile = Profile::BareMetal;
214
215 assert!(profile.validate_features().is_ok());
217 assert!(profile.validate_config(&config, None).is_ok());
218 }
219
220 #[test]
221 fn test_embedded_requires_storage_dir() {
222 let config = LaminarConfig::default();
223 let result = Profile::Embedded.validate_config(&config, None);
224 assert!(result.is_err());
225 assert!(matches!(
226 result.unwrap_err(),
227 ProfileError::RequirementNotMet(_)
228 ));
229 }
230
231 #[test]
232 fn test_durable_fails_without_object_store_url() {
233 let config = LaminarConfig::default();
234 let result = Profile::Durable.validate_config(&config, None);
235 assert!(result.is_err());
236 assert!(matches!(
237 result.unwrap_err(),
238 ProfileError::RequirementNotMet(_)
239 ));
240 }
241
242 #[test]
243 fn test_profile_from_str() {
244 assert_eq!(Profile::from_str("bare_metal").unwrap(), Profile::BareMetal);
245 assert_eq!(Profile::from_str("baremetal").unwrap(), Profile::BareMetal);
246 assert_eq!(Profile::from_str("bare-metal").unwrap(), Profile::BareMetal);
247 assert_eq!(Profile::from_str("embedded").unwrap(), Profile::Embedded);
248 assert_eq!(Profile::from_str("durable").unwrap(), Profile::Durable);
249 assert_eq!(Profile::from_str("delta").unwrap(), Profile::Delta);
250 assert_eq!(Profile::from_str("DURABLE").unwrap(), Profile::Durable);
252 assert!(Profile::from_str("quantum").is_err());
254 assert!(matches!(
255 Profile::from_str("quantum").unwrap_err(),
256 ProfileError::UnknownProfileName(_)
257 ));
258 }
259
260 #[test]
261 fn test_all_profiles_validate_features() {
262 assert!(Profile::BareMetal.validate_features().is_ok());
264 assert!(Profile::Embedded.validate_features().is_ok());
265 assert!(Profile::Durable.validate_features().is_ok());
266 assert!(Profile::Delta.validate_features().is_ok());
267 }
268
269 #[test]
270 fn test_profile_display() {
271 assert_eq!(Profile::BareMetal.to_string(), "bare_metal");
272 assert_eq!(Profile::Embedded.to_string(), "embedded");
273 assert_eq!(Profile::Durable.to_string(), "durable");
274 assert_eq!(Profile::Delta.to_string(), "delta");
275 }
276
277 #[test]
278 fn test_profile_default() {
279 assert_eq!(Profile::default(), Profile::BareMetal);
280 }
281
282 #[test]
283 fn test_apply_defaults_bare_metal_noop() {
284 let mut config = LaminarConfig::default();
285 let original_buffer = config.default_buffer_size;
286 Profile::BareMetal.apply_defaults(&mut config);
287 assert_eq!(config.default_buffer_size, original_buffer);
288 }
289
290 #[test]
291 fn test_apply_defaults_does_not_override_user_values() {
292 let mut config = LaminarConfig {
293 default_buffer_size: 999,
294 ..LaminarConfig::default()
295 };
296 Profile::Durable.apply_defaults(&mut config);
297 assert_eq!(config.default_buffer_size, 999);
299 }
300
301 #[test]
302 fn test_from_config_bare_metal() {
303 let config = LaminarConfig::default();
304 assert_eq!(Profile::from_config(&config, false), Profile::BareMetal);
305 }
306
307 #[test]
308 fn test_from_config_embedded_storage_dir() {
309 let config = LaminarConfig {
310 storage_dir: Some(std::path::PathBuf::from("/tmp/data")),
311 ..LaminarConfig::default()
312 };
313 assert_eq!(Profile::from_config(&config, false), Profile::Embedded);
314 }
315
316 #[test]
317 fn test_from_config_embedded_file_url() {
318 let config = LaminarConfig {
319 object_store_url: Some("file:///tmp/checkpoints".to_string()),
320 ..LaminarConfig::default()
321 };
322 assert_eq!(Profile::from_config(&config, false), Profile::Embedded);
323 }
324
325 #[test]
326 fn test_from_config_durable_s3() {
327 let config = LaminarConfig {
328 object_store_url: Some("s3://my-bucket/prefix".to_string()),
329 ..LaminarConfig::default()
330 };
331 assert_eq!(Profile::from_config(&config, false), Profile::Durable);
332 }
333
334 #[test]
335 fn test_from_config_durable_gs() {
336 let config = LaminarConfig {
337 object_store_url: Some("gs://my-bucket/prefix".to_string()),
338 ..LaminarConfig::default()
339 };
340 assert_eq!(Profile::from_config(&config, false), Profile::Durable);
341 }
342
343 #[test]
344 fn test_from_config_durable_az() {
345 let config = LaminarConfig {
346 object_store_url: Some("az://container/prefix".to_string()),
347 ..LaminarConfig::default()
348 };
349 assert_eq!(Profile::from_config(&config, false), Profile::Durable);
350 }
351
352 #[test]
353 fn test_from_config_durable_abfs() {
354 let config = LaminarConfig {
355 object_store_url: Some("abfs://container/prefix".to_string()),
356 ..LaminarConfig::default()
357 };
358 assert_eq!(Profile::from_config(&config, false), Profile::Durable);
359 }
360
361 #[test]
362 fn test_from_config_delta() {
363 let config = LaminarConfig::default();
364 assert_eq!(Profile::from_config(&config, true), Profile::Delta);
365 }
366
367 #[test]
368 fn test_from_config_delta_overrides_url() {
369 let config = LaminarConfig {
370 object_store_url: Some("s3://bucket/prefix".to_string()),
371 ..LaminarConfig::default()
372 };
373 assert_eq!(Profile::from_config(&config, true), Profile::Delta);
375 }
376}