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}