Skip to main content

vibe_ready/api/
engine_config.rs

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/// Configuration used when creating a [`crate::VibeEngine`].
17#[derive(Clone, Debug)]
18pub struct VibeEngineConfig {
19    /// Platform identifier used by integrations and logs.
20    pub platform_type: VibePlatformType,
21    /// Root directory where vibe-ready stores app data.
22    pub store_root_path: PathBuf,
23    /// Whether persistent stores should use encryption when supported.
24    pub is_encrypt: bool,
25    /// Application identity used for namespacing local data.
26    pub app: VibeAppConfig,
27    /// Logging backend and retention configuration.
28    pub log: VibeLogConfig,
29    /// Work-store backend and storage configuration.
30    pub store: VibeStoreConfig,
31    /// Runtime worker and queue configuration.
32    pub runtime: VibeRuntimeConfig,
33}
34
35/// Builder for [`VibeEngineConfig`].
36#[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/// Application identity used to isolate SDK data on disk.
47#[derive(Clone, Debug)]
48pub struct VibeAppConfig {
49    /// Application name used as the final data-directory segment.
50    pub app_name: String,
51    /// Namespace used to separate environments, tenants, or products.
52    pub namespace: String,
53}
54
55/// Logging behavior used by the SDK.
56#[derive(Clone, Debug)]
57pub struct VibeLogConfig {
58    /// Backend used to persist log entries.
59    pub backend: VibeLogBackend,
60    /// Minimum level emitted by the logger.
61    pub level: LogLevel,
62    /// Whether logs should be written to the selected store backend.
63    pub write_to_store: bool,
64    /// Whether logs should also be printed to stdout.
65    pub output_stdout: bool,
66    /// Number of days log data should be retained by supported backends.
67    pub retention_days: u32,
68    /// Maximum rows retained by supported log backends.
69    pub max_rows: usize,
70}
71
72/// Log backend selected for SDK log persistence.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum VibeLogBackend {
75    /// Disable persistent logging while keeping in-process callbacks available.
76    Noop,
77    /// Persist logs through Diesel using SQLite.
78    DieselSqlite,
79}
80
81/// Store backend selected for SDK persistence.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum VibeStoreBackend {
84    /// Disable persistent key-value storage.
85    Noop,
86    /// Persist key-value data through Diesel using SQLite.
87    DieselSqlite,
88}
89
90/// Backup behavior for SDK managed storage.
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum VibeBackupStrategy {
93    /// Do not create SDK-managed backups.
94    Disabled,
95    /// Reserve backup operations for explicit application control.
96    Manual,
97}
98
99/// Storage behavior used by the SDK.
100#[derive(Clone, Debug)]
101pub struct VibeStoreConfig {
102    /// Backend used for work/key-value persistence.
103    pub backend: VibeStoreBackend,
104    /// Whether storage encryption should be requested when supported.
105    pub encrypt: bool,
106    /// Backup behavior for SDK-managed storage.
107    pub backup_strategy: VibeBackupStrategy,
108}
109
110/// Runtime sizing and queue capacity used by [`crate::VibeEngine`].
111#[derive(Clone, Debug)]
112pub struct VibeRuntimeConfig {
113    /// Number of Tokio worker threads for the engine-owned runtime.
114    pub worker_threads: usize,
115    /// Number of threads used for callback dispatch.
116    pub callback_threads: usize,
117    /// Capacity of the fire-and-forget async task queue.
118    pub async_queue_capacity: usize,
119    /// Capacity of the synchronous invoke queue.
120    pub sync_queue_capacity: usize,
121    /// Capacity per priority lane used by the B9 task scheduler
122    /// (high / normal / low). Each lane is sized identically.
123    pub priority_queue_capacity: usize,
124}
125
126impl VibeEngineConfig {
127    /// Starts building a [`VibeEngineConfig`] with production-ready defaults.
128    ///
129    /// # Returns
130    ///
131    /// A [`VibeEngineConfigBuilder`] that can be customized before calling
132    /// [`VibeEngineConfigBuilder::build`].
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use vibe_ready::{VibeEngineConfig, VibePlatformType};
138    ///
139    /// let config = VibeEngineConfig::builder()
140    ///     .platform(VibePlatformType::MacOS)
141    ///     .app_name("demo")
142    ///     .namespace("examples")
143    ///     .build();
144    /// assert_eq!(config.app_name(), "demo");
145    /// ```
146    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    /// Returns the configured root directory for SDK data.
158    ///
159    /// # Returns
160    ///
161    /// A borrowed [`PathBuf`] pointing to the storage root.
162    pub fn store_path(&self) -> &PathBuf {
163        &self.store_root_path
164    }
165
166    /// Returns whether storage encryption was requested.
167    ///
168    /// # Returns
169    ///
170    /// `true` when encryption is enabled in the store configuration.
171    pub fn is_encrypt(&self) -> bool {
172        self.is_encrypt
173    }
174
175    /// Returns the configured platform identifier.
176    ///
177    /// # Returns
178    ///
179    /// The [`VibePlatformType`] stored in this configuration.
180    pub fn platform(&self) -> VibePlatformType {
181        self.platform_type
182    }
183
184    /// Returns the application name used for data isolation.
185    ///
186    /// # Returns
187    ///
188    /// A borrowed application name string.
189    pub fn app_name(&self) -> &str {
190        &self.app.app_name
191    }
192
193    /// Returns the namespace used for data isolation.
194    ///
195    /// # Returns
196    ///
197    /// A borrowed namespace string.
198    pub fn namespace(&self) -> &str {
199        &self.app.namespace
200    }
201
202    /// Returns the logging configuration.
203    ///
204    /// # Returns
205    ///
206    /// A borrowed [`VibeLogConfig`].
207    pub fn log_config(&self) -> &VibeLogConfig {
208        &self.log
209    }
210
211    /// Returns the storage configuration.
212    ///
213    /// # Returns
214    ///
215    /// A borrowed [`VibeStoreConfig`].
216    pub fn store_config(&self) -> &VibeStoreConfig {
217        &self.store
218    }
219
220    /// Returns the runtime configuration.
221    ///
222    /// # Returns
223    ///
224    /// A borrowed [`VibeRuntimeConfig`].
225    pub fn runtime_config(&self) -> &VibeRuntimeConfig {
226        &self.runtime
227    }
228
229    /// Builds the app-specific storage directory path.
230    ///
231    /// # Returns
232    ///
233    /// `store_root_path / namespace / app_name` as a [`PathBuf`].
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use std::path::PathBuf;
239    /// use vibe_ready::VibeEngineConfig;
240    ///
241    /// let config = VibeEngineConfig::builder()
242    ///     .store_root_path("/tmp/vibe-ready")
243    ///     .namespace("dev")
244    ///     .app_name("app")
245    ///     .build();
246    /// assert_eq!(config.app_store_path(), PathBuf::from("/tmp/vibe-ready/dev/app"));
247    /// ```
248    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    /// Validates identifiers, backend availability, and runtime capacities.
255    ///
256    /// # Returns
257    ///
258    /// `Ok(())` when the configuration can be used to create an engine, or
259    /// [`VibeEngineError`] with configuration context when invalid.
260    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    /// Sets the host platform identifier.
328    ///
329    /// # Returns
330    ///
331    /// The updated builder.
332    pub fn platform(mut self, platform: VibePlatformType) -> Self {
333        self.platform_type = platform;
334        self
335    }
336
337    /// Sets the root directory used for SDK data.
338    ///
339    /// # Returns
340    ///
341    /// The updated builder.
342    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    /// Sets the application name.
348    ///
349    /// # Returns
350    ///
351    /// The updated builder.
352    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    /// Sets the namespace used to isolate data.
358    ///
359    /// # Returns
360    ///
361    /// The updated builder.
362    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
363        self.app.namespace = namespace.into();
364        self
365    }
366
367    /// Replaces the full application configuration.
368    ///
369    /// # Returns
370    ///
371    /// The updated builder.
372    pub fn app_config(mut self, app: VibeAppConfig) -> Self {
373        self.app = app;
374        self
375    }
376
377    /// Replaces the full logging configuration.
378    ///
379    /// # Returns
380    ///
381    /// The updated builder.
382    pub fn log_config(mut self, log: VibeLogConfig) -> Self {
383        self.log = log;
384        self
385    }
386
387    /// Sets the logging backend.
388    ///
389    /// # Returns
390    ///
391    /// The updated builder.
392    pub fn log_backend(mut self, backend: VibeLogBackend) -> Self {
393        self.log.backend = backend;
394        self
395    }
396
397    /// Sets the logging level.
398    ///
399    /// # Returns
400    ///
401    /// The updated builder.
402    pub fn log_level(mut self, level: LogLevel) -> Self {
403        self.log.level = level;
404        self
405    }
406
407    /// Enables or disables log persistence.
408    ///
409    /// # Returns
410    ///
411    /// The updated builder.
412    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    /// Enables or disables stdout logging.
418    ///
419    /// # Returns
420    ///
421    /// The updated builder.
422    pub fn log_output_stdout(mut self, output_stdout: bool) -> Self {
423        self.log.output_stdout = output_stdout;
424        self
425    }
426
427    /// Sets log retention in days for supported backends.
428    ///
429    /// # Returns
430    ///
431    /// The updated builder.
432    pub fn log_retention_days(mut self, retention_days: u32) -> Self {
433        self.log.retention_days = retention_days;
434        self
435    }
436
437    /// Sets the maximum retained log rows for supported backends.
438    ///
439    /// # Returns
440    ///
441    /// The updated builder.
442    pub fn log_max_rows(mut self, max_rows: usize) -> Self {
443        self.log.max_rows = max_rows;
444        self
445    }
446
447    /// Replaces the full storage configuration.
448    ///
449    /// # Returns
450    ///
451    /// The updated builder.
452    pub fn store_config(mut self, store: VibeStoreConfig) -> Self {
453        self.store = store;
454        self
455    }
456
457    /// Sets the key-value store backend.
458    ///
459    /// # Returns
460    ///
461    /// The updated builder.
462    pub fn store_backend(mut self, backend: VibeStoreBackend) -> Self {
463        self.store.backend = backend;
464        self
465    }
466
467    /// Sets the backup strategy for SDK-managed storage.
468    ///
469    /// # Returns
470    ///
471    /// The updated builder.
472    pub fn backup_strategy(mut self, backup_strategy: VibeBackupStrategy) -> Self {
473        self.store.backup_strategy = backup_strategy;
474        self
475    }
476
477    /// Replaces the full runtime configuration.
478    ///
479    /// # Returns
480    ///
481    /// The updated builder.
482    pub fn runtime_config(mut self, runtime: VibeRuntimeConfig) -> Self {
483        self.runtime = runtime;
484        self
485    }
486
487    /// Sets the Tokio worker-thread count for an engine-owned runtime.
488    ///
489    /// # Returns
490    ///
491    /// The updated builder.
492    pub fn runtime_worker_threads(mut self, worker_threads: usize) -> Self {
493        self.runtime.worker_threads = worker_threads;
494        self
495    }
496
497    /// Sets the callback thread-pool size.
498    ///
499    /// # Returns
500    ///
501    /// The updated builder.
502    pub fn callback_threads(mut self, callback_threads: usize) -> Self {
503        self.runtime.callback_threads = callback_threads;
504        self
505    }
506
507    /// Sets async and sync queue capacities.
508    ///
509    /// # Returns
510    ///
511    /// The updated builder.
512    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    /// Sets the per-lane priority queue capacity.
523    ///
524    /// # Returns
525    ///
526    /// The updated builder.
527    pub fn priority_queue_capacity(mut self, capacity: usize) -> Self {
528        self.runtime.priority_queue_capacity = capacity;
529        self
530    }
531
532    /// Enables or disables storage encryption when supported by the backend.
533    ///
534    /// # Returns
535    ///
536    /// The updated builder.
537    pub fn encrypt(mut self, encrypt: bool) -> Self {
538        self.store.encrypt = encrypt;
539        self
540    }
541
542    /// Finalizes the builder into a [`VibeEngineConfig`].
543    ///
544    /// # Returns
545    ///
546    /// The completed configuration. Call [`VibeEngineConfig::validate`] or
547    /// [`crate::VibeEngine::create`] to validate it.
548    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}