this/server/builder.rs
1//! ServerBuilder for fluent API to build HTTP servers
2
3use super::entity_registry::EntityRegistry;
4use super::exposure::RestExposure;
5use super::host::ServerHost;
6use crate::config::LinksConfig;
7use crate::core::events::EventBus;
8use crate::core::module::Module;
9use crate::core::service::LinkService;
10use crate::core::{EntityCreator, EntityFetcher};
11use anyhow::Result;
12use axum::Router;
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::net::TcpListener;
16
17/// Builder for creating HTTP servers with auto-registered routes
18///
19/// # Example
20///
21/// ```ignore
22/// let app = ServerBuilder::new()
23/// .with_link_service(InMemoryLinkService::new())
24/// .register_module(MyModule)
25/// .build()?;
26/// ```
27pub struct ServerBuilder {
28 link_service: Option<Arc<dyn LinkService>>,
29 entity_registry: EntityRegistry,
30 configs: Vec<LinksConfig>,
31 modules: Vec<Arc<dyn Module>>,
32 custom_routes: Vec<Router>,
33 event_bus: Option<EventBus>,
34}
35
36impl ServerBuilder {
37 /// Create a new ServerBuilder
38 pub fn new() -> Self {
39 Self {
40 link_service: None,
41 entity_registry: EntityRegistry::new(),
42 configs: Vec::new(),
43 modules: Vec::new(),
44 custom_routes: Vec::new(),
45 event_bus: None,
46 }
47 }
48
49 /// Set the link service (required)
50 pub fn with_link_service(mut self, service: impl LinkService + 'static) -> Self {
51 self.link_service = Some(Arc::new(service));
52 self
53 }
54
55 /// Add custom routes to the server
56 ///
57 /// Use this to add routes that don't fit the CRUD pattern, such as:
58 /// - Authentication endpoints (/login, /logout)
59 /// - OAuth flows (/oauth/token, /oauth/callback)
60 /// - Webhooks (/webhooks/stripe)
61 /// - Custom business logic endpoints
62 ///
63 /// # Example
64 ///
65 /// ```ignore
66 /// use axum::{Router, routing::{post, get}, Json};
67 /// use serde_json::json;
68 ///
69 /// let auth_routes = Router::new()
70 /// .route("/login", post(login_handler))
71 /// .route("/logout", post(logout_handler))
72 /// .route("/oauth/token", post(oauth_token_handler));
73 ///
74 /// ServerBuilder::new()
75 /// .with_link_service(service)
76 /// .with_custom_routes(auth_routes)
77 /// .register_module(module)?
78 /// .build()?;
79 /// ```
80 pub fn with_custom_routes(mut self, routes: Router) -> Self {
81 self.custom_routes.push(routes);
82 self
83 }
84
85 /// Enable the event bus for real-time notifications
86 ///
87 /// When enabled, REST/GraphQL handlers will publish events for mutations,
88 /// and real-time exposures (WebSocket, SSE) can subscribe to receive them.
89 ///
90 /// # Arguments
91 ///
92 /// * `capacity` - Buffer size for the broadcast channel (recommended: 1024)
93 ///
94 /// # Example
95 ///
96 /// ```ignore
97 /// ServerBuilder::new()
98 /// .with_link_service(service)
99 /// .with_event_bus(1024)
100 /// .register_module(module)?
101 /// .build_host()?;
102 /// ```
103 pub fn with_event_bus(mut self, capacity: usize) -> Self {
104 self.event_bus = Some(EventBus::new(capacity));
105 self
106 }
107
108 /// Register a module
109 ///
110 /// This will:
111 /// 1. Load the module's configuration
112 /// 2. Register all entities from the module
113 /// 3. Store the module for entity fetching
114 pub fn register_module(mut self, module: impl Module + 'static) -> Result<Self> {
115 let module = Arc::new(module);
116
117 // Load the module's configuration
118 let config = module.links_config()?;
119 self.configs.push(config);
120
121 // Register entities from the module
122 module.register_entities(&mut self.entity_registry);
123
124 // Store module for fetchers
125 self.modules.push(module);
126
127 Ok(self)
128 }
129
130 /// Build the transport-agnostic host
131 ///
132 /// This generates a `ServerHost` that can be used with any exposure type
133 /// (REST, GraphQL, gRPC, etc.).
134 ///
135 /// # Returns
136 ///
137 /// Returns a `ServerHost` containing all framework state.
138 pub fn build_host(mut self) -> Result<ServerHost> {
139 // Merge all configs
140 let merged_config = self.merge_configs()?;
141
142 // Extract link service
143 let link_service = self
144 .link_service
145 .take()
146 .ok_or_else(|| anyhow::anyhow!("LinkService is required. Call .with_link_service()"))?;
147
148 // Build entity fetchers map from all modules
149 let mut fetchers_map: HashMap<String, Arc<dyn EntityFetcher>> = HashMap::new();
150 for module in &self.modules {
151 for entity_type in module.entity_types() {
152 if let Some(fetcher) = module.get_entity_fetcher(entity_type) {
153 fetchers_map.insert(entity_type.to_string(), fetcher);
154 }
155 }
156 }
157
158 // Build entity creators map from all modules
159 let mut creators_map: HashMap<String, Arc<dyn EntityCreator>> = HashMap::new();
160 for module in &self.modules {
161 for entity_type in module.entity_types() {
162 if let Some(creator) = module.get_entity_creator(entity_type) {
163 creators_map.insert(entity_type.to_string(), creator);
164 }
165 }
166 }
167
168 // Build the host
169 let mut host = ServerHost::from_builder_components(
170 link_service,
171 merged_config,
172 self.entity_registry,
173 fetchers_map,
174 creators_map,
175 )?;
176
177 // Attach event bus if configured
178 if let Some(event_bus) = self.event_bus.take() {
179 host = host.with_event_bus(event_bus);
180 }
181
182 Ok(host)
183 }
184
185 /// Build the final REST router
186 ///
187 /// This generates:
188 /// - CRUD routes for all registered entities
189 /// - Link routes (bidirectional)
190 /// - Introspection routes
191 ///
192 /// Note: This is a convenience method that builds the host and immediately
193 /// exposes it via REST. For other exposure types, use `build_host_arc()`.
194 pub fn build(mut self) -> Result<Router> {
195 let custom_routes = std::mem::take(&mut self.custom_routes);
196 let host = Arc::new(self.build_host()?);
197 RestExposure::build_router(host, custom_routes)
198 }
199
200 /// Merge all configurations from registered modules
201 fn merge_configs(&self) -> Result<LinksConfig> {
202 Ok(LinksConfig::merge(self.configs.clone()))
203 }
204
205 /// Serve the application with graceful shutdown
206 ///
207 /// This will:
208 /// - Bind to the provided address
209 /// - Start serving requests
210 /// - Handle SIGTERM and SIGINT (Ctrl+C) for graceful shutdown
211 ///
212 /// # Example
213 ///
214 /// ```ignore
215 /// ServerBuilder::new()
216 /// .with_link_service(service)
217 /// .register_module(module)?
218 /// .serve("127.0.0.1:3000").await?;
219 /// ```
220 pub async fn serve(self, addr: &str) -> Result<()> {
221 let app = self.build()?;
222 let listener = TcpListener::bind(addr).await?;
223
224 tracing::info!("Server listening on {}", addr);
225
226 axum::serve(listener, app)
227 .with_graceful_shutdown(shutdown_signal())
228 .await?;
229
230 tracing::info!("Server shutdown complete");
231 Ok(())
232 }
233}
234
235impl Default for ServerBuilder {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241/// Wait for shutdown signal (SIGTERM or Ctrl+C)
242async fn shutdown_signal() {
243 use tokio::signal;
244
245 let ctrl_c = async {
246 signal::ctrl_c()
247 .await
248 .expect("failed to install Ctrl+C handler");
249 };
250
251 #[cfg(unix)]
252 let terminate = async {
253 signal::unix::signal(signal::unix::SignalKind::terminate())
254 .expect("failed to install SIGTERM handler")
255 .recv()
256 .await;
257 };
258
259 #[cfg(not(unix))]
260 let terminate = std::future::pending::<()>();
261
262 tokio::select! {
263 _ = ctrl_c => {
264 tracing::info!("Received Ctrl+C signal, initiating graceful shutdown...");
265 },
266 _ = terminate => {
267 tracing::info!("Received SIGTERM signal, initiating graceful shutdown...");
268 },
269 }
270}