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