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    /// The realtime subsystem is enabled but no entities are declared.
52    /// Clients will not be able to subscribe to any entity streams.
53    RealtimeWithNoEntities,
54}
55
56impl fmt::Display for SubsystemConfigWarning {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::LocalStorageInProduction => f.write_str(
60                "local filesystem storage is active — not suitable for multi-instance \
61                 deployments; use S3, GCS, or Azure Blob in production",
62            ),
63            Self::UnknownBucket { name } => write!(
64                f,
65                "bucket '{name}' is declared in the schema config but not found in the \
66                 runtime storage state — requests for this bucket will return 404; \
67                 ensure the bucket name matches exactly"
68            ),
69            Self::EmptyFunctionsRegistry => f.write_str(
70                "the functions module directory is configured but no function \
71                 definitions exist in the schema; the directory will be ignored",
72            ),
73            Self::RealtimeWithNoEntities => f.write_str(
74                "the realtime subsystem is enabled but no entities are declared; \
75                 clients will receive errors for all subscribe requests — \
76                 add entities under the 'realtime.entities' key in the compiled schema",
77            ),
78        }
79    }
80}
81
82// ── Validator ─────────────────────────────────────────────────────────────────
83
84/// Inspect `subsystems` for common misconfigurations and return advisory
85/// warnings.
86///
87/// The returned list is empty when no problems are detected. Warnings are
88/// non-fatal: callers decide how to surface them.
89#[must_use]
90pub fn validate_subsystems_config(subsystems: &ServerSubsystems) -> Vec<SubsystemConfigWarning> {
91    let mut warnings = Vec::new();
92
93    if let Some(storage) = &subsystems.storage {
94        check_storage(&mut warnings, storage);
95    }
96
97    if let Some(functions) = &subsystems.functions {
98        if functions.config.definitions.is_empty() {
99            warnings.push(SubsystemConfigWarning::EmptyFunctionsRegistry);
100        }
101    }
102
103    if let Some(realtime) = &subsystems.realtime {
104        if realtime.schema_config.entities.is_empty() {
105            warnings.push(SubsystemConfigWarning::RealtimeWithNoEntities);
106        }
107    }
108
109    warnings
110}
111
112/// Check the storage subsystem for warnings.
113fn check_storage(warnings: &mut Vec<SubsystemConfigWarning>, storage: &StorageSubsystem) {
114    // Warn when using local filesystem storage (not production-safe)
115    if matches!(*storage.state.backend, StorageBackend::Local(_)) {
116        warnings.push(SubsystemConfigWarning::LocalStorageInProduction);
117    }
118
119    // Warn about buckets declared in schema config but absent from runtime state
120    check_unknown_buckets(warnings, &storage.schema_config, storage);
121}
122
123/// Emit a warning for each schema-declared bucket not present in runtime state.
124fn check_unknown_buckets(
125    warnings: &mut Vec<SubsystemConfigWarning>,
126    schema_config: &SchemaStorageConfig,
127    storage: &StorageSubsystem,
128) {
129    for bucket_def in &schema_config.buckets {
130        if !storage.state.buckets.contains_key(&bucket_def.name) {
131            warnings.push(SubsystemConfigWarning::UnknownBucket {
132                name: bucket_def.name.clone(),
133            });
134        }
135    }
136}