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}