1use crate::api::engine_error::{VibeEngineError, VibeEngineErrorCode};
2use crate::api::platform_type::VibePlatformType;
3use crate::log::log_level::LogLevel;
4use crate::utils::global_ref::{ENGINE_CHANNEL_BUFFER_SIZE, ENGINE_SYNC_CHANNEL_BUFFER_SIZE};
5use std::path::{Path, PathBuf};
6use std::{fmt, fmt::Formatter};
7
8const DEFAULT_APP_NAME: &str = "vibe-ready-app";
9const DEFAULT_NAMESPACE: &str = "default";
10const DEFAULT_RUNTIME_WORKER_THREADS: usize = 4;
11const DEFAULT_CALLBACK_THREADS: usize = 3;
12const DEFAULT_LOG_RETENTION_DAYS: u32 = 7;
13const DEFAULT_LOG_MAX_ROWS: usize = 120_000;
14const DEFAULT_PRIORITY_QUEUE_CAPACITY: usize = 1024;
15
16#[derive(Clone, Debug)]
18pub struct VibeEngineConfig {
19 pub platform_type: VibePlatformType,
21 pub store_root_path: PathBuf,
23 pub is_encrypt: bool,
25 pub app: VibeAppConfig,
27 pub log: VibeLogConfig,
29 pub store: VibeStoreConfig,
31 pub runtime: VibeRuntimeConfig,
33}
34
35#[derive(Clone, Debug)]
37pub struct VibeEngineConfigBuilder {
38 platform_type: VibePlatformType,
39 store_root_path: PathBuf,
40 app: VibeAppConfig,
41 log: VibeLogConfig,
42 store: VibeStoreConfig,
43 runtime: VibeRuntimeConfig,
44}
45
46#[derive(Clone, Debug)]
48pub struct VibeAppConfig {
49 pub app_name: String,
51 pub namespace: String,
53}
54
55#[derive(Clone, Debug)]
57pub struct VibeLogConfig {
58 pub backend: VibeLogBackend,
60 pub level: LogLevel,
62 pub write_to_store: bool,
64 pub output_stdout: bool,
66 pub retention_days: u32,
68 pub max_rows: usize,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum VibeLogBackend {
75 Noop,
77 DieselSqlite,
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum VibeStoreBackend {
84 Noop,
86 DieselSqlite,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum VibeBackupStrategy {
93 Disabled,
95 Manual,
97}
98
99#[derive(Clone, Debug)]
101pub struct VibeStoreConfig {
102 pub backend: VibeStoreBackend,
104 pub encrypt: bool,
106 pub backup_strategy: VibeBackupStrategy,
108}
109
110#[derive(Clone, Debug)]
112pub struct VibeRuntimeConfig {
113 pub worker_threads: usize,
115 pub callback_threads: usize,
117 pub async_queue_capacity: usize,
119 pub sync_queue_capacity: usize,
121 pub priority_queue_capacity: usize,
124}
125
126impl VibeEngineConfig {
127 pub fn builder() -> VibeEngineConfigBuilder {
147 VibeEngineConfigBuilder {
148 platform_type: Default::default(),
149 store_root_path: default_store_root_path(),
150 app: VibeAppConfig::default(),
151 log: VibeLogConfig::default(),
152 store: VibeStoreConfig::default(),
153 runtime: VibeRuntimeConfig::default(),
154 }
155 }
156
157 pub fn store_path(&self) -> &PathBuf {
163 &self.store_root_path
164 }
165
166 pub fn is_encrypt(&self) -> bool {
172 self.is_encrypt
173 }
174
175 pub fn platform(&self) -> VibePlatformType {
181 self.platform_type
182 }
183
184 pub fn app_name(&self) -> &str {
190 &self.app.app_name
191 }
192
193 pub fn namespace(&self) -> &str {
199 &self.app.namespace
200 }
201
202 pub fn log_config(&self) -> &VibeLogConfig {
208 &self.log
209 }
210
211 pub fn store_config(&self) -> &VibeStoreConfig {
217 &self.store
218 }
219
220 pub fn runtime_config(&self) -> &VibeRuntimeConfig {
226 &self.runtime
227 }
228
229 pub fn app_store_path(&self) -> PathBuf {
249 self.store_root_path
250 .join(&self.app.namespace)
251 .join(&self.app.app_name)
252 }
253
254 pub fn validate(&self) -> Result<(), VibeEngineError> {
261 validate_identifier("app_name", &self.app.app_name)?;
262 validate_identifier("namespace", &self.app.namespace)?;
263
264 if self.store_root_path.as_os_str().is_empty() {
265 return Err(config_error("store_root_path must not be empty"));
266 }
267 validate_log_backend(self.log.backend)?;
268 validate_store_backend(self.store.backend)?;
269 if self.log.retention_days == 0 {
270 return Err(config_error("log.retention_days must be greater than zero"));
271 }
272 if self.log.max_rows == 0 {
273 return Err(config_error("log.max_rows must be greater than zero"));
274 }
275 if self.runtime.worker_threads == 0 {
276 return Err(config_error(
277 "runtime.worker_threads must be greater than zero",
278 ));
279 }
280 if self.runtime.callback_threads == 0 {
281 return Err(config_error(
282 "runtime.callback_threads must be greater than zero",
283 ));
284 }
285 if self.runtime.async_queue_capacity == 0 {
286 return Err(config_error(
287 "runtime.async_queue_capacity must be greater than zero",
288 ));
289 }
290 if self.runtime.sync_queue_capacity == 0 {
291 return Err(config_error(
292 "runtime.sync_queue_capacity must be greater than zero",
293 ));
294 }
295 if self.runtime.priority_queue_capacity == 0 {
296 return Err(config_error(
297 "runtime.priority_queue_capacity must be greater than zero",
298 ));
299 }
300
301 Ok(())
302 }
303}
304
305impl fmt::Display for VibeEngineConfig {
306 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
307 write!(
308 f,
309 "VibeEngineConfig {{ app_name: {}, namespace: {}, platform: {}, store_root_path: {}, is_encrypt: {}, log_backend: {:?}, log_level: {:?}, store_backend: {:?}, worker_threads: {}, callback_threads: {}, async_queue_capacity: {}, sync_queue_capacity: {} }}",
310 self.app.app_name,
311 self.app.namespace,
312 self.platform_type.to_i32(),
313 self.store_root_path.display(),
314 self.is_encrypt,
315 self.log.backend,
316 self.log.level,
317 self.store.backend,
318 self.runtime.worker_threads,
319 self.runtime.callback_threads,
320 self.runtime.async_queue_capacity,
321 self.runtime.sync_queue_capacity,
322 )
323 }
324}
325
326impl VibeEngineConfigBuilder {
327 pub fn platform(mut self, platform: VibePlatformType) -> Self {
333 self.platform_type = platform;
334 self
335 }
336
337 pub fn store_root_path(mut self, path: impl AsRef<Path>) -> Self {
343 self.store_root_path = path.as_ref().to_path_buf();
344 self
345 }
346
347 pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
353 self.app.app_name = app_name.into();
354 self
355 }
356
357 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
363 self.app.namespace = namespace.into();
364 self
365 }
366
367 pub fn app_config(mut self, app: VibeAppConfig) -> Self {
373 self.app = app;
374 self
375 }
376
377 pub fn log_config(mut self, log: VibeLogConfig) -> Self {
383 self.log = log;
384 self
385 }
386
387 pub fn log_backend(mut self, backend: VibeLogBackend) -> Self {
393 self.log.backend = backend;
394 self
395 }
396
397 pub fn log_level(mut self, level: LogLevel) -> Self {
403 self.log.level = level;
404 self
405 }
406
407 pub fn log_write_to_store(mut self, write_to_store: bool) -> Self {
413 self.log.write_to_store = write_to_store;
414 self
415 }
416
417 pub fn log_output_stdout(mut self, output_stdout: bool) -> Self {
423 self.log.output_stdout = output_stdout;
424 self
425 }
426
427 pub fn log_retention_days(mut self, retention_days: u32) -> Self {
433 self.log.retention_days = retention_days;
434 self
435 }
436
437 pub fn log_max_rows(mut self, max_rows: usize) -> Self {
443 self.log.max_rows = max_rows;
444 self
445 }
446
447 pub fn store_config(mut self, store: VibeStoreConfig) -> Self {
453 self.store = store;
454 self
455 }
456
457 pub fn store_backend(mut self, backend: VibeStoreBackend) -> Self {
463 self.store.backend = backend;
464 self
465 }
466
467 pub fn backup_strategy(mut self, backup_strategy: VibeBackupStrategy) -> Self {
473 self.store.backup_strategy = backup_strategy;
474 self
475 }
476
477 pub fn runtime_config(mut self, runtime: VibeRuntimeConfig) -> Self {
483 self.runtime = runtime;
484 self
485 }
486
487 pub fn runtime_worker_threads(mut self, worker_threads: usize) -> Self {
493 self.runtime.worker_threads = worker_threads;
494 self
495 }
496
497 pub fn callback_threads(mut self, callback_threads: usize) -> Self {
503 self.runtime.callback_threads = callback_threads;
504 self
505 }
506
507 pub fn queue_capacity(
513 mut self,
514 async_queue_capacity: usize,
515 sync_queue_capacity: usize,
516 ) -> Self {
517 self.runtime.async_queue_capacity = async_queue_capacity;
518 self.runtime.sync_queue_capacity = sync_queue_capacity;
519 self
520 }
521
522 pub fn priority_queue_capacity(mut self, capacity: usize) -> Self {
528 self.runtime.priority_queue_capacity = capacity;
529 self
530 }
531
532 pub fn encrypt(mut self, encrypt: bool) -> Self {
538 self.store.encrypt = encrypt;
539 self
540 }
541
542 pub fn build(self) -> VibeEngineConfig {
549 VibeEngineConfig {
550 platform_type: self.platform_type,
551 store_root_path: self.store_root_path,
552 is_encrypt: self.store.encrypt,
553 app: self.app,
554 log: self.log,
555 store: self.store,
556 runtime: self.runtime,
557 }
558 }
559}
560
561impl Default for VibeAppConfig {
562 fn default() -> Self {
563 Self {
564 app_name: DEFAULT_APP_NAME.to_string(),
565 namespace: DEFAULT_NAMESPACE.to_string(),
566 }
567 }
568}
569
570impl Default for VibeLogConfig {
571 fn default() -> Self {
572 Self {
573 backend: default_log_backend(),
574 level: LogLevel::Info,
575 write_to_store: true,
576 output_stdout: true,
577 retention_days: DEFAULT_LOG_RETENTION_DAYS,
578 max_rows: DEFAULT_LOG_MAX_ROWS,
579 }
580 }
581}
582
583impl Default for VibeLogBackend {
584 fn default() -> Self {
585 default_log_backend()
586 }
587}
588
589impl Default for VibeStoreBackend {
590 fn default() -> Self {
591 default_store_backend()
592 }
593}
594
595impl Default for VibeStoreConfig {
596 fn default() -> Self {
597 Self {
598 backend: default_store_backend(),
599 encrypt: false,
600 backup_strategy: VibeBackupStrategy::Disabled,
601 }
602 }
603}
604
605impl Default for VibeRuntimeConfig {
606 fn default() -> Self {
607 Self {
608 worker_threads: DEFAULT_RUNTIME_WORKER_THREADS,
609 callback_threads: DEFAULT_CALLBACK_THREADS,
610 async_queue_capacity: ENGINE_CHANNEL_BUFFER_SIZE,
611 sync_queue_capacity: ENGINE_SYNC_CHANNEL_BUFFER_SIZE,
612 priority_queue_capacity: DEFAULT_PRIORITY_QUEUE_CAPACITY,
613 }
614 }
615}
616
617fn default_store_root_path() -> PathBuf {
618 std::env::var_os("VIBE_READY_HOME")
619 .map(PathBuf::from)
620 .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".vibe-ready")))
621 .unwrap_or_else(|| std::env::temp_dir().join("vibe-ready"))
622}
623
624fn default_log_backend() -> VibeLogBackend {
625 if cfg!(feature = "log-diesel") {
626 VibeLogBackend::DieselSqlite
627 } else {
628 VibeLogBackend::Noop
629 }
630}
631
632fn default_store_backend() -> VibeStoreBackend {
633 if cfg!(feature = "store-diesel-sqlite") {
634 VibeStoreBackend::DieselSqlite
635 } else {
636 VibeStoreBackend::Noop
637 }
638}
639
640fn validate_identifier(field: &str, value: &str) -> Result<(), VibeEngineError> {
641 let trimmed = value.trim();
642 if trimmed.is_empty() {
643 return Err(config_error(format!("{field} must not be empty")));
644 }
645 if trimmed == "." || value.contains('/') || value.contains('\\') || value.contains("..") {
646 return Err(config_error(format!(
647 "{field} must not contain path separators, '.', or '..'"
648 )));
649 }
650 Ok(())
651}
652
653fn validate_log_backend(backend: VibeLogBackend) -> Result<(), VibeEngineError> {
654 match backend {
655 VibeLogBackend::Noop => Ok(()),
656 VibeLogBackend::DieselSqlite if cfg!(feature = "log-diesel") => Ok(()),
657 _ => Err(config_error(format!(
658 "log.backend {:?} is not enabled by current feature set",
659 backend
660 ))),
661 }
662}
663
664fn validate_store_backend(backend: VibeStoreBackend) -> Result<(), VibeEngineError> {
665 match backend {
666 VibeStoreBackend::Noop => Ok(()),
667 VibeStoreBackend::DieselSqlite if cfg!(feature = "store-diesel-sqlite") => Ok(()),
668 _ => Err(config_error(format!(
669 "store.backend {:?} is not enabled by current feature set",
670 backend
671 ))),
672 }
673}
674
675fn config_error(message: impl Into<String>) -> VibeEngineError {
676 VibeEngineError::from_error_code_msg(VibeEngineErrorCode::ConfigError, message.into())
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682
683 #[test]
684 fn default_config_is_valid_and_uses_app_scoped_path() -> Result<(), VibeEngineError> {
685 let config = VibeEngineConfig::builder().build();
686
687 config.validate()?;
688 assert_eq!(config.app_name(), DEFAULT_APP_NAME);
689 assert_eq!(config.namespace(), DEFAULT_NAMESPACE);
690 assert!(config.app_store_path().ends_with(DEFAULT_APP_NAME));
691 Ok(())
692 }
693
694 #[test]
695 fn validate_rejects_invalid_namespace_and_queue_capacity() {
696 let invalid_namespace = VibeEngineConfig::builder().namespace("../bad").build();
697 assert_eq!(
698 invalid_namespace.validate().unwrap_err().code(),
699 VibeEngineErrorCode::ConfigError.code()
700 );
701
702 let invalid_queue = VibeEngineConfig::builder().queue_capacity(0, 1).build();
703 assert_eq!(
704 invalid_queue.validate().unwrap_err().code(),
705 VibeEngineErrorCode::ConfigError.code()
706 );
707 }
708
709 #[test]
710 fn validate_accepts_noop_backends() -> Result<(), VibeEngineError> {
711 let config = VibeEngineConfig::builder()
712 .log_backend(VibeLogBackend::Noop)
713 .store_backend(VibeStoreBackend::Noop)
714 .build();
715
716 config.validate()
717 }
718
719 #[cfg(not(feature = "log-diesel"))]
720 #[test]
721 fn validate_rejects_diesel_log_backend_when_feature_is_disabled() {
722 let config = VibeEngineConfig::builder()
723 .log_backend(VibeLogBackend::DieselSqlite)
724 .build();
725
726 assert_eq!(
727 config.validate().unwrap_err().code(),
728 VibeEngineErrorCode::ConfigError.code()
729 );
730 }
731
732 #[cfg(not(feature = "store-diesel-sqlite"))]
733 #[test]
734 fn validate_rejects_diesel_store_backend_when_feature_is_disabled() {
735 let config = VibeEngineConfig::builder()
736 .store_backend(VibeStoreBackend::DieselSqlite)
737 .build();
738
739 assert_eq!(
740 config.validate().unwrap_err().code(),
741 VibeEngineErrorCode::ConfigError.code()
742 );
743 }
744}
745
746#[cfg(test)]
747mod strict_tests {
748 use super::*;
749 include!(concat!(
750 env!("CARGO_MANIFEST_DIR"),
751 "/test/unit/api/engine_config_tests.rs"
752 ));
753}