Skip to main content

fraiseql_server/subsystems/
builder.rs

1//! Builder for assembling server subsystems with cross-subsystem validation.
2
3use super::{FunctionsSubsystem, 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///     .build()?;
37/// ```
38#[derive(Default)]
39pub struct ServerSubsystemsBuilder {
40    storage:   Option<StorageSubsystem>,
41    functions: Option<FunctionsSubsystem>,
42}
43
44impl ServerSubsystemsBuilder {
45    /// Create a new builder with no subsystems registered.
46    #[must_use = "builder does nothing until .build() is called"]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Register the storage subsystem.
52    #[must_use = "builder method returns modified builder"]
53    pub fn with_storage(mut self, subsystem: StorageSubsystem) -> Self {
54        self.storage = Some(subsystem);
55        self
56    }
57
58    /// Register the functions subsystem.
59    #[must_use = "builder method returns modified builder"]
60    pub fn with_functions(mut self, subsystem: FunctionsSubsystem) -> Self {
61        self.functions = Some(subsystem);
62        self
63    }
64
65    /// Validate cross-subsystem dependencies and build [`ServerSubsystems`].
66    ///
67    /// # Errors
68    ///
69    /// Returns [`SubsystemBuildError::MissingDependency`] if the functions subsystem
70    /// contains `after:storage` triggers but no storage subsystem has been registered.
71    pub fn build(self) -> Result<ServerSubsystems, SubsystemBuildError> {
72        self.validate()?;
73        Ok(ServerSubsystems {
74            storage:   self.storage,
75            functions: self.functions,
76        })
77    }
78
79    /// Check cross-subsystem dependency constraints.
80    fn validate(&self) -> Result<(), SubsystemBuildError> {
81        // If functions define after:storage triggers, the storage subsystem must be present.
82        if let Some(functions) = &self.functions {
83            let has_storage_triggers = functions
84                .config
85                .definitions
86                .iter()
87                .any(|d| d.trigger.starts_with("after:storage:"));
88
89            if has_storage_triggers && self.storage.is_none() {
90                return Err(SubsystemBuildError::MissingDependency {
91                    dependant:  "functions",
92                    dependency: "storage",
93                    reason:     "one or more function definitions use after:storage triggers but \
94                             no storage subsystem is configured; either add a [storage] section \
95                             to the compiled schema or remove the after:storage triggers"
96                        .to_string(),
97                });
98            }
99        }
100        Ok(())
101    }
102}