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}