Skip to main content

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}