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}