Skip to main content

fraiseql_server/subsystems/
builder.rs

1//! Builder for assembling server subsystems with cross-subsystem validation.
2
3use super::{FunctionsSubsystem, RealtimeSubsystem, ServerSubsystems, StorageSubsystem};
4
5/// Error returned when `ServerSubsystemsBuilder::build` detects a configuration problem.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum SubsystemBuildError {
9    /// A required dependency between subsystems is not satisfied.
10    ///
11    /// For example: functions define `after:storage` triggers but no storage subsystem
12    /// was registered with the builder.
13    #[error("{dependant} requires {dependency}: {reason}")]
14    MissingDependency {
15        /// The subsystem that has an unmet dependency.
16        dependant:  &'static str,
17        /// The subsystem that is missing.
18        dependency: &'static str,
19        /// Human-readable explanation.
20        reason:     String,
21    },
22}
23
24/// Builder for [`ServerSubsystems`].
25///
26/// Use the fluent API to register each optional subsystem, then call
27/// [`build`][Self::build] to validate cross-subsystem dependencies and
28/// produce the final [`ServerSubsystems`].
29///
30/// # Example
31///
32/// ```rust,ignore
33/// let subsystems = ServerSubsystemsBuilder::new()
34///     .with_storage(storage_subsystem)
35///     .with_functions(functions_subsystem)
36///     .with_realtime(realtime_subsystem)
37///     .build()?;
38/// ```
39#[derive(Default)]
40pub struct ServerSubsystemsBuilder {
41    storage:   Option<StorageSubsystem>,
42    functions: Option<FunctionsSubsystem>,
43    realtime:  Option<RealtimeSubsystem>,
44}
45
46impl ServerSubsystemsBuilder {
47    /// Create a new builder with no subsystems registered.
48    #[must_use = "builder does nothing until .build() is called"]
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Register the storage subsystem.
54    #[must_use = "builder method returns modified builder"]
55    pub fn with_storage(mut self, subsystem: StorageSubsystem) -> Self {
56        self.storage = Some(subsystem);
57        self
58    }
59
60    /// Register the functions subsystem.
61    #[must_use = "builder method returns modified builder"]
62    pub fn with_functions(mut self, subsystem: FunctionsSubsystem) -> Self {
63        self.functions = Some(subsystem);
64        self
65    }
66
67    /// Register the realtime subsystem.
68    #[must_use = "builder method returns modified builder"]
69    pub fn with_realtime(mut self, subsystem: RealtimeSubsystem) -> Self {
70        self.realtime = Some(subsystem);
71        self
72    }
73
74    /// Validate cross-subsystem dependencies and build [`ServerSubsystems`].
75    ///
76    /// # Errors
77    ///
78    /// Returns [`SubsystemBuildError::MissingDependency`] if the functions subsystem
79    /// contains `after:storage` triggers but no storage subsystem has been registered.
80    pub fn build(self) -> Result<ServerSubsystems, SubsystemBuildError> {
81        self.validate()?;
82        Ok(ServerSubsystems {
83            storage:   self.storage,
84            functions: self.functions,
85            realtime:  self.realtime,
86        })
87    }
88
89    /// Check cross-subsystem dependency constraints.
90    fn validate(&self) -> Result<(), SubsystemBuildError> {
91        // If functions define after:storage triggers, the storage subsystem must be present.
92        if let Some(functions) = &self.functions {
93            let has_storage_triggers = functions
94                .config
95                .definitions
96                .iter()
97                .any(|d| d.trigger.starts_with("after:storage:"));
98
99            if has_storage_triggers && self.storage.is_none() {
100                return Err(SubsystemBuildError::MissingDependency {
101                    dependant:  "functions",
102                    dependency: "storage",
103                    reason:     "one or more function definitions use after:storage triggers but \
104                             no storage subsystem is configured; either add a [storage] section \
105                             to the compiled schema or remove the after:storage triggers"
106                        .to_string(),
107                });
108            }
109        }
110        Ok(())
111    }
112}