Skip to main content

fraiseql_server/subsystems/
validator.rs

1//! Cross-subsystem configuration validation and startup warnings.
2//!
3//! [`validate_subsystems_config`] inspects the assembled [`ServerSubsystems`]
4//! for common misconfigurations and emits human-readable warnings. It does
5//! **not** abort startup — callers decide how to surface warnings (log them,
6//! return them in a health response, fail hard in tests, etc.).
7//!
8//! # When to call
9//!
10//! Call once during server startup, after `ServerSubsystemsBuilder::build`
11//! succeeds but before the server begins accepting requests:
12//!
13//! ```rust,ignore
14//! let warnings = validate_subsystems_config(&subsystems);
15//! for w in &warnings {
16//!     tracing::warn!(warning = %w, "Subsystems config advisory");
17//! }
18//! ```
19
20use std::fmt;
21
22use fraiseql_storage::backend::StorageBackend;
23
24use super::{ServerSubsystems, StorageSubsystem};
25use crate::schema::loader::SchemaStorageConfig;
26
27// ── Warning type ──────────────────────────────────────────────────────────────
28
29/// A non-fatal configuration advisory emitted by [`validate_subsystems_config`].
30///
31/// Warnings do not prevent startup, but callers should log them and may choose
32/// to expose them in health-check responses.
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum SubsystemConfigWarning {
36    /// Local filesystem storage is active. Not suitable for multi-instance
37    /// deployments — use S3, GCS, or Azure Blob in production.
38    LocalStorageInProduction,
39
40    /// A bucket is declared in the schema config but does not appear in the
41    /// runtime storage state. Requests for this bucket will return 404.
42    UnknownBucket {
43        /// Bucket name referenced in the schema config.
44        name: String,
45    },
46
47    /// The functions module directory is configured but no function
48    /// definitions exist in the schema. The directory will be ignored.
49    EmptyFunctionsRegistry,
50}
51
52impl fmt::Display for SubsystemConfigWarning {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::LocalStorageInProduction => f.write_str(
56                "local filesystem storage is active — not suitable for multi-instance \
57                 deployments; use S3, GCS, or Azure Blob in production",
58            ),
59            Self::UnknownBucket { name } => write!(
60                f,
61                "bucket '{name}' is declared in the schema config but not found in the \
62                 runtime storage state — requests for this bucket will return 404; \
63                 ensure the bucket name matches exactly"
64            ),
65            Self::EmptyFunctionsRegistry => f.write_str(
66                "the functions module directory is configured but no function \
67                 definitions exist in the schema; the directory will be ignored",
68            ),
69        }
70    }
71}
72
73// ── Validator ─────────────────────────────────────────────────────────────────
74
75/// Inspect `subsystems` for common misconfigurations and return advisory
76/// warnings.
77///
78/// The returned list is empty when no problems are detected. Warnings are
79/// non-fatal: callers decide how to surface them.
80#[must_use]
81pub fn validate_subsystems_config(subsystems: &ServerSubsystems) -> Vec<SubsystemConfigWarning> {
82    let mut warnings = Vec::new();
83
84    if let Some(storage) = &subsystems.storage {
85        check_storage(&mut warnings, storage);
86    }
87
88    if let Some(functions) = &subsystems.functions {
89        if functions.config.definitions.is_empty() {
90            warnings.push(SubsystemConfigWarning::EmptyFunctionsRegistry);
91        }
92    }
93
94    warnings
95}
96
97/// Check the storage subsystem for warnings.
98fn check_storage(warnings: &mut Vec<SubsystemConfigWarning>, storage: &StorageSubsystem) {
99    // Warn when using local filesystem storage (not production-safe)
100    if matches!(*storage.state.backend, StorageBackend::Local(_)) {
101        warnings.push(SubsystemConfigWarning::LocalStorageInProduction);
102    }
103
104    // Warn about buckets declared in schema config but absent from runtime state
105    check_unknown_buckets(warnings, &storage.schema_config, storage);
106}
107
108/// Emit a warning for each schema-declared bucket not present in runtime state.
109fn check_unknown_buckets(
110    warnings: &mut Vec<SubsystemConfigWarning>,
111    schema_config: &SchemaStorageConfig,
112    storage: &StorageSubsystem,
113) {
114    for bucket_def in &schema_config.buckets {
115        if !storage.state.buckets.contains_key(&bucket_def.name) {
116            warnings.push(SubsystemConfigWarning::UnknownBucket {
117                name: bucket_def.name.clone(),
118            });
119        }
120    }
121}