Skip to main content

this/server/
router.rs

1//! Router builder utilities for link routes and protocol merging
2
3use crate::core::query::QueryParams;
4use crate::links::handlers::{
5    AppState, create_link, create_linked_entity, delete_link, get_link, get_link_by_route,
6    handle_nested_path_get, list_available_links, list_links, update_link,
7};
8use axum::{Router, extract::Query, routing::get};
9
10/// Combine a REST router and a gRPC router into a single router.
11///
12/// This function safely merges the two routers by taking advantage of the fact
13/// that the gRPC router was built with
14/// [`GrpcExposure::build_router_no_fallback`](crate::server::exposure::grpc::GrpcExposure::build_router_no_fallback),
15/// which does **not** install a fallback handler.
16///
17/// The REST router's fallback (used for deeply nested link paths) is preserved,
18/// ensuring that both REST and gRPC routes work correctly on the same server.
19///
20/// # Arguments
21///
22/// * `rest_router` - The REST router (from [`RestExposure::build_router`](crate::server::exposure::RestExposure::build_router)),
23///   which includes a fallback for nested link paths.
24/// * `grpc_router` - The gRPC router (from [`GrpcExposure::build_router_no_fallback`](crate::server::exposure::grpc::GrpcExposure::build_router_no_fallback)),
25///   which has **no** fallback.
26///
27/// # Example
28///
29/// ```rust,ignore
30/// use this::server::exposure::{RestExposure, grpc::GrpcExposure};
31/// use this::server::router::combine_rest_and_grpc;
32///
33/// let host = Arc::new(builder.build_host()?);
34/// let rest_router = RestExposure::build_router(host.clone(), vec![])?;
35/// let grpc_router = GrpcExposure::build_router_no_fallback(host)?;
36/// let app = combine_rest_and_grpc(rest_router, grpc_router);
37///
38/// axum::serve(listener, app).await?;
39/// ```
40///
41/// # Panics
42///
43/// Panics if `grpc_router` has a fallback installed (e.g., if built with
44/// `GrpcExposure::build_router()` instead of `build_router_no_fallback()`).
45/// Always use `build_router_no_fallback()` for the gRPC side.
46#[cfg(feature = "grpc")]
47pub fn combine_rest_and_grpc(rest_router: Router, grpc_router: Router) -> Router {
48    rest_router.merge(grpc_router)
49}
50
51/// Build link routes from configuration
52///
53/// These routes are generic and work for all entities using semantic route_names:
54/// - GET /links/{link_id} - Get a specific link by ID
55/// - GET /{entity_type}/{entity_id}/{route_name} - List links (e.g., /users/123/cars-owned)
56/// - POST /{entity_type}/{entity_id}/{route_name} - Create new entity + link (entity + metadata in body)
57/// - GET /{source_type}/{source_id}/{route_name}/{target_id} - Get a specific link (e.g., /users/123/cars-owned/456)
58/// - POST /{source_type}/{source_id}/{route_name}/{target_id} - Create link between existing entities
59/// - PUT /{source_type}/{source_id}/{route_name}/{target_id} - Update link metadata
60/// - DELETE /{source_type}/{source_id}/{route_name}/{target_id} - Delete link
61/// - GET /{entity_type}/{entity_id}/links - List available link types
62///
63/// NOTE: Nested routes are supported up to 2 levels automatically:
64/// - GET /{entity_type}/{entity_id}/{route_name} - List linked entities
65/// - GET /{entity_type}/{entity_id}/{route_name}/{target_id} - Get specific link
66///
67/// For deeper nesting (3+ levels), see: docs/guides/CUSTOM_NESTED_ROUTES.md
68///
69/// The route_name (e.g., "cars-owned", "cars-driven") is resolved to the appropriate
70/// link_type (e.g., "owner", "driver") automatically by the LinkRouteRegistry.
71pub fn build_link_routes(state: AppState) -> Router {
72    use axum::extract::{Path as AxumPath, Request, State as AxumState};
73    use axum::response::IntoResponse;
74    use uuid::Uuid;
75
76    // Handler intelligent qui route vers list_links OU handle_nested_path_get selon la profondeur
77    let smart_handler = |AxumState(state): AxumState<AppState>,
78                         AxumPath((entity_type_plural, entity_id, route_name)): AxumPath<(
79        String,
80        Uuid,
81        String,
82    )>,
83                         Query(params): Query<QueryParams>,
84                         req: Request| async move {
85        let path = req.uri().path();
86        let segments: Vec<&str> = path.trim_matches('/').split('/').collect();
87
88        // Si plus de 3 segments, c'est une route imbriquée à 3+ niveaux
89        if segments.len() >= 5 {
90            // Utiliser le handler générique pour chemins profonds (with pagination)
91            handle_nested_path_get(AxumState(state), AxumPath(path.to_string()), Query(params))
92                .await
93                .map(|r| r.into_response())
94        } else {
95            // Route classique à 2 niveaux - with pagination
96            list_links(
97                AxumState(state),
98                AxumPath((entity_type_plural, entity_id, route_name)),
99                Query(params),
100            )
101            .await
102            .map(|r| r.into_response())
103        }
104    };
105
106    // Handler fallback pour les autres cas (with pagination)
107    let fallback_handler = |AxumState(state): AxumState<AppState>,
108                            Query(params): Query<QueryParams>,
109                            req: Request| async move {
110        let path = req.uri().path().to_string();
111        handle_nested_path_get(AxumState(state), AxumPath(path), Query(params))
112            .await
113            .map(|r| r.into_response())
114    };
115
116    Router::new()
117        .route("/links/{link_id}", get(get_link))
118        .route(
119            "/{entity_type}/{entity_id}/{route_name}",
120            get(smart_handler).post(create_linked_entity),
121        )
122        .route(
123            "/{source_type}/{source_id}/{route_name}/{target_id}",
124            get(get_link_by_route)
125                .post(create_link)
126                .put(update_link)
127                .delete(delete_link),
128        )
129        .route(
130            "/{entity_type}/{entity_id}/links",
131            get(list_available_links),
132        )
133        .fallback(fallback_handler)
134        .with_state(state)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::config::LinksConfig;
141    use crate::core::events::EventBus;
142    use crate::links::handlers::AppState;
143    use crate::links::registry::LinkRouteRegistry;
144    use crate::storage::InMemoryLinkService;
145    use std::collections::HashMap;
146    use std::sync::Arc;
147
148    /// Build a minimal AppState for testing
149    fn test_app_state() -> AppState {
150        let config = Arc::new(LinksConfig::default_config());
151        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
152        AppState {
153            link_service: Arc::new(InMemoryLinkService::new()),
154            config,
155            registry,
156            entity_fetchers: Arc::new(HashMap::new()),
157            entity_creators: Arc::new(HashMap::new()),
158            event_bus: None,
159        }
160    }
161
162    #[test]
163    fn test_build_link_routes_produces_router() {
164        let state = test_app_state();
165        let router = build_link_routes(state);
166        // Should not panic; router is valid
167        let _ = router;
168    }
169
170    #[test]
171    fn test_build_link_routes_with_event_bus() {
172        let config = Arc::new(LinksConfig::default_config());
173        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
174        let state = AppState {
175            link_service: Arc::new(InMemoryLinkService::new()),
176            config,
177            registry,
178            entity_fetchers: Arc::new(HashMap::new()),
179            entity_creators: Arc::new(HashMap::new()),
180            event_bus: Some(Arc::new(EventBus::new(16))),
181        };
182        let router = build_link_routes(state);
183        let _ = router;
184    }
185
186    #[test]
187    fn test_build_link_routes_empty_config() {
188        let config = Arc::new(LinksConfig {
189            entities: vec![],
190            links: vec![],
191            validation_rules: None,
192            events: None,
193            sinks: None,
194        });
195        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
196        let state = AppState {
197            link_service: Arc::new(InMemoryLinkService::new()),
198            config,
199            registry,
200            entity_fetchers: Arc::new(HashMap::new()),
201            entity_creators: Arc::new(HashMap::new()),
202            event_bus: None,
203        };
204        let router = build_link_routes(state);
205        let _ = router;
206    }
207
208    #[cfg(feature = "grpc")]
209    mod grpc_tests {
210        use super::super::combine_rest_and_grpc;
211        use axum::Router;
212
213        #[test]
214        fn test_combine_rest_and_grpc_merges_routers() {
215            let rest = Router::new();
216            let grpc = Router::new();
217            let combined = combine_rest_and_grpc(rest, grpc);
218            let _ = combined;
219        }
220    }
221}