Skip to main content

fastapi_core/
api_router.rs

1//! APIRouter for modular route organization.
2//!
3//! This module provides [`APIRouter`] for grouping related routes with
4//! shared configuration like prefixes, tags, and dependencies.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use fastapi_core::api_router::APIRouter;
10//! use fastapi_core::{Request, Response, RequestContext};
11//!
12//! async fn get_users(ctx: &RequestContext, req: &mut Request) -> Response {
13//!     Response::ok().body_text("List of users")
14//! }
15//!
16//! async fn create_user(ctx: &RequestContext, req: &mut Request) -> Response {
17//!     Response::ok().body_text("User created")
18//! }
19//!
20//! let router = APIRouter::new()
21//!     .prefix("/api/v1/users")
22//!     .tags(vec!["users"])
23//!     .get("", get_users)
24//!     .post("", create_user);
25//!
26//! let app = App::builder()
27//!     .include_router(router)
28//!     .build();
29//! ```
30
31use std::collections::HashMap;
32use std::future::Future;
33use std::pin::Pin;
34use std::sync::Arc;
35
36use crate::app::{BoxHandler, RouteEntry};
37use crate::context::RequestContext;
38use crate::request::{Method, Request};
39use crate::response::Response;
40
41/// Response definition for OpenAPI documentation.
42#[derive(Debug, Clone)]
43pub struct ResponseDef {
44    /// HTTP status code description.
45    pub description: String,
46    /// Optional example response body.
47    pub example: Option<serde_json::Value>,
48    /// Content type for this response.
49    pub content_type: Option<String>,
50}
51
52impl ResponseDef {
53    /// Create a new response definition with a description.
54    #[must_use]
55    pub fn new(description: impl Into<String>) -> Self {
56        Self {
57            description: description.into(),
58            example: None,
59            content_type: None,
60        }
61    }
62
63    /// Set an example response body.
64    #[must_use]
65    pub fn with_example(mut self, example: serde_json::Value) -> Self {
66        self.example = Some(example);
67        self
68    }
69
70    /// Set the content type.
71    #[must_use]
72    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
73        self.content_type = Some(content_type.into());
74        self
75    }
76}
77
78/// A boxed dependency function.
79///
80/// Dependencies are executed before route handlers and can short-circuit
81/// the request by returning an error response.
82pub type BoxDependency = Arc<
83    dyn Fn(
84            &RequestContext,
85            &mut Request,
86        ) -> Pin<Box<dyn Future<Output = Result<(), Response>> + Send>>
87        + Send
88        + Sync,
89>;
90
91/// A shared dependency that runs before route handlers.
92///
93/// Dependencies can be used for authentication, validation, or any
94/// pre-processing that should apply to all routes in a router.
95#[derive(Clone)]
96pub struct RouterDependency {
97    /// The dependency function.
98    pub(crate) handler: BoxDependency,
99    /// Name for debugging/logging.
100    pub(crate) name: String,
101}
102
103impl RouterDependency {
104    /// Create a new router dependency.
105    ///
106    /// The function should return `Ok(())` to continue processing,
107    /// or `Err(Response)` to short-circuit with an error response.
108    pub fn new<F, Fut>(name: impl Into<String>, f: F) -> Self
109    where
110        F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
111        Fut: Future<Output = Result<(), Response>> + Send + 'static,
112    {
113        Self {
114            handler: Arc::new(move |ctx, req| Box::pin(f(ctx, req))),
115            name: name.into(),
116        }
117    }
118
119    /// Execute the dependency.
120    pub async fn execute(&self, ctx: &RequestContext, req: &mut Request) -> Result<(), Response> {
121        (self.handler)(ctx, req).await
122    }
123}
124
125impl std::fmt::Debug for RouterDependency {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("RouterDependency")
128            .field("name", &self.name)
129            .finish_non_exhaustive()
130    }
131}
132
133/// Configuration for including a router with overrides.
134///
135/// When including a router in an app or another router, this config
136/// allows overriding or prepending settings to the included router.
137///
138/// # Merge Rules (per FastAPI spec)
139///
140/// 1. **prefix**: Prepended to router's prefix
141/// 2. **tags**: Prepended to router's tags
142/// 3. **dependencies**: Prepended to router's dependencies
143/// 4. **responses**: Merged (route > router > config)
144/// 5. **deprecated**: Override if provided
145/// 6. **include_in_schema**: Override if provided
146///
147/// # Example
148///
149/// ```ignore
150/// let config = IncludeConfig::new()
151///     .prefix("/api/v1")
152///     .tags(vec!["api"])
153///     .dependency(auth_dep);
154///
155/// let app = App::builder()
156///     .include_router_with_config(router, config)
157///     .build();
158/// ```
159#[derive(Debug, Default, Clone)]
160pub struct IncludeConfig {
161    /// Prefix to prepend to the router's prefix.
162    prefix: Option<String>,
163    /// Tags to prepend to the router's tags.
164    tags: Vec<String>,
165    /// Dependencies to prepend to the router's dependencies.
166    dependencies: Vec<RouterDependency>,
167    /// Response definitions to merge.
168    responses: HashMap<u16, ResponseDef>,
169    /// Override for deprecated flag.
170    deprecated: Option<bool>,
171    /// Override for include_in_schema flag.
172    include_in_schema: Option<bool>,
173}
174
175impl IncludeConfig {
176    /// Creates a new empty include configuration.
177    #[must_use]
178    pub fn new() -> Self {
179        Self::default()
180    }
181
182    /// Sets the prefix to prepend to the router's prefix.
183    #[must_use]
184    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
185        let p = prefix.into();
186        if !p.is_empty() {
187            // Normalize prefix
188            let normalized = if p.starts_with('/') {
189                p
190            } else {
191                format!("/{}", p)
192            };
193            // Remove trailing slash
194            let normalized = if normalized.ends_with('/') && normalized.len() > 1 {
195                normalized.trim_end_matches('/').to_string()
196            } else {
197                normalized
198            };
199            self.prefix = Some(normalized);
200        }
201        self
202    }
203
204    /// Sets the tags to prepend to the router's tags.
205    #[must_use]
206    pub fn tags(mut self, tags: Vec<impl Into<String>>) -> Self {
207        self.tags = tags.into_iter().map(Into::into).collect();
208        self
209    }
210
211    /// Adds a single tag to prepend.
212    #[must_use]
213    pub fn tag(mut self, tag: impl Into<String>) -> Self {
214        self.tags.push(tag.into());
215        self
216    }
217
218    /// Adds a dependency to prepend.
219    #[must_use]
220    pub fn dependency(mut self, dep: RouterDependency) -> Self {
221        self.dependencies.push(dep);
222        self
223    }
224
225    /// Sets dependencies to prepend.
226    #[must_use]
227    pub fn dependencies(mut self, deps: Vec<RouterDependency>) -> Self {
228        self.dependencies = deps;
229        self
230    }
231
232    /// Adds a response definition.
233    #[must_use]
234    pub fn response(mut self, status_code: u16, def: ResponseDef) -> Self {
235        self.responses.insert(status_code, def);
236        self
237    }
238
239    /// Sets response definitions.
240    #[must_use]
241    pub fn responses(mut self, responses: HashMap<u16, ResponseDef>) -> Self {
242        self.responses = responses;
243        self
244    }
245
246    /// Sets the deprecated override.
247    #[must_use]
248    pub fn deprecated(mut self, deprecated: bool) -> Self {
249        self.deprecated = Some(deprecated);
250        self
251    }
252
253    /// Sets the include_in_schema override.
254    #[must_use]
255    pub fn include_in_schema(mut self, include: bool) -> Self {
256        self.include_in_schema = Some(include);
257        self
258    }
259
260    /// Returns the prefix.
261    #[must_use]
262    pub fn get_prefix(&self) -> Option<&str> {
263        self.prefix.as_deref()
264    }
265
266    /// Returns the tags.
267    #[must_use]
268    pub fn get_tags(&self) -> &[String] {
269        &self.tags
270    }
271
272    /// Returns the dependencies.
273    #[must_use]
274    pub fn get_dependencies(&self) -> &[RouterDependency] {
275        &self.dependencies
276    }
277
278    /// Returns the responses.
279    #[must_use]
280    pub fn get_responses(&self) -> &HashMap<u16, ResponseDef> {
281        &self.responses
282    }
283
284    /// Returns the deprecated override.
285    #[must_use]
286    pub fn get_deprecated(&self) -> Option<bool> {
287        self.deprecated
288    }
289
290    /// Returns the include_in_schema override.
291    #[must_use]
292    pub fn get_include_in_schema(&self) -> Option<bool> {
293        self.include_in_schema
294    }
295}
296
297/// Internal route storage that includes router-level metadata.
298#[derive(Clone)]
299pub struct RouterRoute {
300    /// The HTTP method.
301    pub method: Method,
302    /// The path (without prefix).
303    pub path: String,
304    /// The handler function.
305    pub(crate) handler: Arc<BoxHandler>,
306    /// Route-specific tags (merged with router tags).
307    pub tags: Vec<String>,
308    /// Route-specific dependencies (run after router dependencies).
309    pub dependencies: Vec<RouterDependency>,
310    /// Whether this route is deprecated.
311    pub deprecated: Option<bool>,
312    /// Whether to include in OpenAPI schema.
313    pub include_in_schema: bool,
314}
315
316impl std::fmt::Debug for RouterRoute {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.debug_struct("RouterRoute")
319            .field("method", &self.method)
320            .field("path", &self.path)
321            .field("tags", &self.tags)
322            .field("deprecated", &self.deprecated)
323            .field("include_in_schema", &self.include_in_schema)
324            .finish_non_exhaustive()
325    }
326}
327
328/// Router for grouping related routes with shared configuration.
329///
330/// `APIRouter` allows you to organize routes into logical groups with
331/// common prefixes, tags, dependencies, and other shared settings.
332///
333/// # Example
334///
335/// ```ignore
336/// let users_router = APIRouter::new()
337///     .prefix("/users")
338///     .tags(vec!["users"])
339///     .get("", list_users)
340///     .get("/{id}", get_user)
341///     .post("", create_user);
342///
343/// let items_router = APIRouter::new()
344///     .prefix("/items")
345///     .tags(vec!["items"])
346///     .get("", list_items);
347///
348/// let app = App::builder()
349///     .include_router(users_router)
350///     .include_router(items_router)
351///     .build();
352/// ```
353#[derive(Debug, Default)]
354pub struct APIRouter {
355    /// URL prefix for all routes.
356    prefix: String,
357    /// Default tags for all routes.
358    tags: Vec<String>,
359    /// Shared dependencies run before every route.
360    dependencies: Vec<RouterDependency>,
361    /// Shared response definitions.
362    responses: HashMap<u16, ResponseDef>,
363    /// Whether all routes are deprecated.
364    deprecated: Option<bool>,
365    /// Whether to include routes in OpenAPI schema.
366    include_in_schema: bool,
367    /// The routes in this router.
368    routes: Vec<RouterRoute>,
369}
370
371impl APIRouter {
372    /// Creates a new empty router.
373    #[must_use]
374    pub fn new() -> Self {
375        Self {
376            prefix: String::new(),
377            tags: Vec::new(),
378            dependencies: Vec::new(),
379            responses: HashMap::new(),
380            deprecated: None,
381            include_in_schema: true,
382            routes: Vec::new(),
383        }
384    }
385
386    /// Creates a new router with the given prefix.
387    #[must_use]
388    pub fn with_prefix(prefix: impl Into<String>) -> Self {
389        Self::new().prefix(prefix)
390    }
391
392    /// Sets the URL prefix for all routes.
393    ///
394    /// The prefix is prepended to all route paths when the router
395    /// is included in an application.
396    #[must_use]
397    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
398        let p = prefix.into();
399        // Ensure prefix starts with / if not empty
400        if !p.is_empty() && !p.starts_with('/') {
401            self.prefix = format!("/{}", p);
402        } else {
403            self.prefix = p;
404        }
405        // Remove trailing slash
406        if self.prefix.ends_with('/') && self.prefix.len() > 1 {
407            self.prefix.pop();
408        }
409        self
410    }
411
412    /// Sets the default tags for all routes.
413    ///
414    /// Tags are used for organizing routes in OpenAPI documentation.
415    /// Route-specific tags are merged with these router-level tags.
416    #[must_use]
417    pub fn tags(mut self, tags: Vec<impl Into<String>>) -> Self {
418        self.tags = tags.into_iter().map(Into::into).collect();
419        self
420    }
421
422    /// Adds a single tag to the default tags.
423    #[must_use]
424    pub fn tag(mut self, tag: impl Into<String>) -> Self {
425        self.tags.push(tag.into());
426        self
427    }
428
429    /// Adds a dependency that runs before all routes.
430    ///
431    /// Dependencies are executed in the order they are added.
432    /// If a dependency returns an error response, subsequent
433    /// dependencies and the route handler are not executed.
434    #[must_use]
435    pub fn dependency(mut self, dep: RouterDependency) -> Self {
436        self.dependencies.push(dep);
437        self
438    }
439
440    /// Adds multiple dependencies.
441    #[must_use]
442    pub fn dependencies(mut self, deps: Vec<RouterDependency>) -> Self {
443        self.dependencies.extend(deps);
444        self
445    }
446
447    /// Adds a response definition for OpenAPI documentation.
448    #[must_use]
449    pub fn response(mut self, status_code: u16, def: ResponseDef) -> Self {
450        self.responses.insert(status_code, def);
451        self
452    }
453
454    /// Sets shared response definitions.
455    #[must_use]
456    pub fn responses(mut self, responses: HashMap<u16, ResponseDef>) -> Self {
457        self.responses = responses;
458        self
459    }
460
461    /// Marks all routes as deprecated.
462    #[must_use]
463    pub fn deprecated(mut self, deprecated: bool) -> Self {
464        self.deprecated = Some(deprecated);
465        self
466    }
467
468    /// Sets whether routes should be included in OpenAPI schema.
469    #[must_use]
470    pub fn include_in_schema(mut self, include: bool) -> Self {
471        self.include_in_schema = include;
472        self
473    }
474
475    /// Adds a route with the given method and path.
476    #[must_use]
477    pub fn route<H, Fut>(mut self, path: impl Into<String>, method: Method, handler: H) -> Self
478    where
479        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
480        Fut: Future<Output = Response> + Send + 'static,
481    {
482        let boxed: BoxHandler = Box::new(move |ctx, req| {
483            let fut = handler(ctx, req);
484            Box::pin(fut)
485        });
486        self.routes.push(RouterRoute {
487            method,
488            path: path.into(),
489            handler: Arc::new(boxed),
490            tags: Vec::new(),
491            dependencies: Vec::new(),
492            deprecated: None,
493            include_in_schema: true,
494        });
495        self
496    }
497
498    /// Adds a GET route.
499    #[must_use]
500    pub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
501    where
502        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
503        Fut: Future<Output = Response> + Send + 'static,
504    {
505        self.route(path, Method::Get, handler)
506    }
507
508    /// Adds a POST route.
509    #[must_use]
510    pub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
511    where
512        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
513        Fut: Future<Output = Response> + Send + 'static,
514    {
515        self.route(path, Method::Post, handler)
516    }
517
518    /// Adds a PUT route.
519    #[must_use]
520    pub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
521    where
522        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
523        Fut: Future<Output = Response> + Send + 'static,
524    {
525        self.route(path, Method::Put, handler)
526    }
527
528    /// Adds a DELETE route.
529    #[must_use]
530    pub fn delete<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
531    where
532        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
533        Fut: Future<Output = Response> + Send + 'static,
534    {
535        self.route(path, Method::Delete, handler)
536    }
537
538    /// Adds a PATCH route.
539    #[must_use]
540    pub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
541    where
542        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
543        Fut: Future<Output = Response> + Send + 'static,
544    {
545        self.route(path, Method::Patch, handler)
546    }
547
548    /// Adds an OPTIONS route.
549    #[must_use]
550    pub fn options<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
551    where
552        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
553        Fut: Future<Output = Response> + Send + 'static,
554    {
555        self.route(path, Method::Options, handler)
556    }
557
558    /// Adds a HEAD route.
559    #[must_use]
560    pub fn head<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
561    where
562        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
563        Fut: Future<Output = Response> + Send + 'static,
564    {
565        self.route(path, Method::Head, handler)
566    }
567
568    /// Includes another router's routes with an optional additional prefix.
569    ///
570    /// This allows nesting routers for hierarchical organization.
571    ///
572    /// # Example
573    ///
574    /// ```ignore
575    /// let v1_users = APIRouter::new()
576    ///     .prefix("/users")
577    ///     .get("", list_users);
578    ///
579    /// let v1_router = APIRouter::new()
580    ///     .prefix("/v1")
581    ///     .include_router(v1_users);
582    ///
583    /// // Routes: /v1/users
584    /// ```
585    #[must_use]
586    pub fn include_router(self, other: APIRouter) -> Self {
587        self.include_router_with_config(other, IncludeConfig::default())
588    }
589
590    /// Includes another router with configuration overrides.
591    ///
592    /// This allows applying additional configuration when including a router,
593    /// such as prepending a prefix, adding tags, or injecting dependencies.
594    ///
595    /// # Merge Rules
596    ///
597    /// Following FastAPI's merge semantics:
598    /// 1. **prefix**: config.prefix + router.prefix + route.path
599    /// 2. **tags**: config.tags + router.tags + route.tags
600    /// 3. **dependencies**: config.deps + router.deps + route.deps
601    /// 4. **responses**: route > router > config (later values win)
602    /// 5. **deprecated**: config overrides router if set
603    /// 6. **include_in_schema**: config overrides router if set
604    ///
605    /// # Example
606    ///
607    /// ```ignore
608    /// let users_router = APIRouter::new()
609    ///     .prefix("/users")
610    ///     .get("", list_users);
611    ///
612    /// let config = IncludeConfig::new()
613    ///     .prefix("/api/v1")
614    ///     .tags(vec!["api"])
615    ///     .dependency(auth_dep);
616    ///
617    /// let app_router = APIRouter::new()
618    ///     .include_router_with_config(users_router, config);
619    ///
620    /// // Routes: /api/v1/users with ["api", "users"] tags
621    /// ```
622    #[must_use]
623    pub fn include_router_with_config(mut self, other: APIRouter, config: IncludeConfig) -> Self {
624        // Determine effective values with config overrides
625        let effective_deprecated = config.deprecated.or(other.deprecated);
626        let effective_include_in_schema =
627            config.include_in_schema.unwrap_or(other.include_in_schema);
628
629        // Build the full prefix: config.prefix + router.prefix
630        let full_prefix = match config.prefix.as_deref() {
631            Some(config_prefix) => combine_paths(config_prefix, &other.prefix),
632            None => other.prefix.clone(),
633        };
634
635        // Merge routes with combined configuration
636        for mut route in other.routes {
637            // Combine path: full_prefix + route.path
638            let combined_path = combine_paths(&full_prefix, &route.path);
639            route.path = combined_path;
640
641            // Merge tags: config.tags + router.tags + route.tags
642            let mut merged_tags = config.tags.clone();
643            merged_tags.extend(other.tags.clone());
644            merged_tags.extend(route.tags);
645            route.tags = merged_tags;
646
647            // Merge dependencies: config.deps + router.deps + route.deps
648            let mut merged_deps = config.dependencies.clone();
649            merged_deps.extend(other.dependencies.clone());
650            merged_deps.extend(route.dependencies);
651            route.dependencies = merged_deps;
652
653            // Apply deprecated override
654            if route.deprecated.is_none() {
655                route.deprecated = effective_deprecated;
656            }
657
658            // Apply include_in_schema override
659            if !effective_include_in_schema {
660                route.include_in_schema = false;
661            }
662
663            self.routes.push(route);
664        }
665
666        // Merge response definitions: route > router > config
667        // First add config responses (lowest priority)
668        for (code, def) in config.responses {
669            self.responses.entry(code).or_insert(def);
670        }
671        // Then add router responses (higher priority)
672        for (code, def) in other.responses {
673            self.responses.insert(code, def);
674        }
675
676        self
677    }
678
679    /// Returns the prefix for this router.
680    #[must_use]
681    pub fn get_prefix(&self) -> &str {
682        &self.prefix
683    }
684
685    /// Returns the tags for this router.
686    #[must_use]
687    pub fn get_tags(&self) -> &[String] {
688        &self.tags
689    }
690
691    /// Returns the dependencies for this router.
692    #[must_use]
693    pub fn get_dependencies(&self) -> &[RouterDependency] {
694        &self.dependencies
695    }
696
697    /// Returns the response definitions.
698    #[must_use]
699    pub fn get_responses(&self) -> &HashMap<u16, ResponseDef> {
700        &self.responses
701    }
702
703    /// Returns whether routes are deprecated.
704    #[must_use]
705    pub fn is_deprecated(&self) -> Option<bool> {
706        self.deprecated
707    }
708
709    /// Returns whether routes should be included in schema.
710    #[must_use]
711    pub fn get_include_in_schema(&self) -> bool {
712        self.include_in_schema
713    }
714
715    /// Returns the routes in this router.
716    #[must_use]
717    pub fn get_routes(&self) -> &[RouterRoute] {
718        &self.routes
719    }
720
721    /// Converts router routes to `RouteEntry` values for the app.
722    ///
723    /// This applies the router's prefix, tags, and dependencies to all routes.
724    /// The returned routes can be added to an `AppBuilder`.
725    #[must_use]
726    pub fn into_route_entries(self) -> Vec<RouteEntry> {
727        let prefix = self.prefix;
728        let _router_tags = self.tags;
729        let router_deps = self.dependencies;
730        let _router_deprecated = self.deprecated;
731        let router_include_in_schema = self.include_in_schema;
732
733        self.routes
734            .into_iter()
735            .filter(|route| {
736                // Only include routes that should be in schema
737                router_include_in_schema && route.include_in_schema
738            })
739            .map(move |route| {
740                // Combine prefix with route path
741                let full_path = combine_paths(&prefix, &route.path);
742
743                // Clone dependencies for the wrapped handler
744                let deps: Vec<RouterDependency> = router_deps
745                    .iter()
746                    .cloned()
747                    .chain(route.dependencies)
748                    .collect();
749
750                let handler = route.handler;
751
752                // Create a wrapper handler that runs dependencies first
753                if deps.is_empty() {
754                    // No dependencies, use handler directly
755                    RouteEntry::new(route.method, full_path, move |ctx, req| {
756                        let handler = Arc::clone(&handler);
757                        (handler)(ctx, req)
758                    })
759                } else {
760                    // Wrap handler to run dependencies first
761                    let deps = Arc::new(deps);
762                    RouteEntry::new(route.method, full_path, move |ctx, req| {
763                        let handler = Arc::clone(&handler);
764                        let deps = Arc::clone(&deps);
765                        Box::pin(async move {
766                            // Run all dependencies
767                            for dep in deps.iter() {
768                                if let Err(response) = dep.execute(ctx, req).await {
769                                    return response;
770                                }
771                            }
772                            // All dependencies passed, run handler
773                            (handler)(ctx, req).await
774                        })
775                    })
776                }
777            })
778            .collect()
779    }
780}
781
782/// Combines two path segments, handling slashes correctly.
783fn combine_paths(prefix: &str, path: &str) -> String {
784    match (prefix.is_empty(), path.is_empty()) {
785        (true, true) => "/".to_string(),
786        (true, false) => {
787            if path.starts_with('/') {
788                path.to_string()
789            } else {
790                format!("/{}", path)
791            }
792        }
793        (false, true) => prefix.to_string(),
794        (false, false) => {
795            let prefix = prefix.trim_end_matches('/');
796            let path = path.trim_start_matches('/');
797            if path.is_empty() {
798                prefix.to_string()
799            } else {
800                format!("{}/{}", prefix, path)
801            }
802        }
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809
810    #[test]
811    fn test_combine_paths() {
812        assert_eq!(combine_paths("", ""), "/");
813        assert_eq!(combine_paths("", "/users"), "/users");
814        assert_eq!(combine_paths("", "users"), "/users");
815        assert_eq!(combine_paths("/api", ""), "/api");
816        assert_eq!(combine_paths("/api", "/users"), "/api/users");
817        assert_eq!(combine_paths("/api", "users"), "/api/users");
818        assert_eq!(combine_paths("/api/", "/users"), "/api/users");
819        assert_eq!(combine_paths("/api/", "users"), "/api/users");
820    }
821
822    #[test]
823    fn test_router_prefix_normalization() {
824        let router = APIRouter::new().prefix("api");
825        assert_eq!(router.get_prefix(), "/api");
826
827        let router = APIRouter::new().prefix("/api/");
828        assert_eq!(router.get_prefix(), "/api");
829
830        let router = APIRouter::new().prefix("/api/v1");
831        assert_eq!(router.get_prefix(), "/api/v1");
832    }
833
834    #[test]
835    fn test_router_tags() {
836        let router = APIRouter::new().tags(vec!["users", "admin"]).tag("api");
837
838        assert_eq!(router.get_tags(), &["users", "admin", "api"]);
839    }
840
841    #[test]
842    fn test_router_deprecated() {
843        let router = APIRouter::new().deprecated(true);
844        assert_eq!(router.is_deprecated(), Some(true));
845
846        let router = APIRouter::new();
847        assert_eq!(router.is_deprecated(), None);
848    }
849
850    #[test]
851    fn test_response_def() {
852        let def = ResponseDef::new("Success")
853            .with_example(serde_json::json!({"id": 1}))
854            .with_content_type("application/json");
855
856        assert_eq!(def.description, "Success");
857        assert_eq!(def.example, Some(serde_json::json!({"id": 1})));
858        assert_eq!(def.content_type, Some("application/json".to_string()));
859    }
860
861    #[test]
862    fn test_include_in_schema() {
863        let router = APIRouter::new().include_in_schema(false);
864        assert!(!router.get_include_in_schema());
865
866        let router = APIRouter::new();
867        assert!(router.get_include_in_schema());
868    }
869
870    #[test]
871    fn test_nested_routers_prefix_combination() {
872        // Create an inner router
873        let inner = APIRouter::new().prefix("/items");
874        assert_eq!(inner.get_prefix(), "/items");
875
876        // Create an outer router and include the inner one
877        let outer = APIRouter::new().prefix("/api/v1").include_router(inner);
878
879        // The routes from inner should have combined prefix
880        // Note: We test this indirectly since routes are private
881        assert_eq!(outer.get_prefix(), "/api/v1");
882    }
883
884    #[test]
885    fn test_router_with_responses() {
886        let router = APIRouter::new()
887            .response(200, ResponseDef::new("Success"))
888            .response(404, ResponseDef::new("Not Found"));
889
890        let responses = router.get_responses();
891        assert_eq!(responses.len(), 2);
892        assert!(responses.contains_key(&200));
893        assert!(responses.contains_key(&404));
894    }
895
896    #[test]
897    fn test_router_dependency_creation() {
898        let dep = RouterDependency::new("auth", |_ctx, _req| async { Ok(()) });
899        assert_eq!(dep.name, "auth");
900    }
901
902    #[test]
903    fn test_router_with_dependency() {
904        let dep = RouterDependency::new("auth", |_ctx, _req| async { Ok(()) });
905
906        let router = APIRouter::new().dependency(dep);
907        assert_eq!(router.get_dependencies().len(), 1);
908        assert_eq!(router.get_dependencies()[0].name, "auth");
909    }
910
911    #[test]
912    fn test_router_multiple_dependencies() {
913        let dep1 = RouterDependency::new("auth", |_ctx, _req| async { Ok(()) });
914        let dep2 = RouterDependency::new("rate_limit", |_ctx, _req| async { Ok(()) });
915
916        let router = APIRouter::new().dependencies(vec![dep1, dep2]);
917        assert_eq!(router.get_dependencies().len(), 2);
918    }
919
920    #[test]
921    fn test_tag_merging_with_nested_routers() {
922        let inner = APIRouter::new().tags(vec!["items"]);
923        let outer = APIRouter::new().tags(vec!["api"]).include_router(inner);
924
925        // Outer router keeps its own tags
926        assert_eq!(outer.get_tags(), &["api"]);
927        // Inner router's tags are merged into its routes (tested via into_route_entries)
928    }
929
930    #[test]
931    fn test_with_prefix_constructor() {
932        let router = APIRouter::with_prefix("/api/v1");
933        assert_eq!(router.get_prefix(), "/api/v1");
934    }
935
936    #[test]
937    fn test_empty_router() {
938        let router = APIRouter::new();
939        assert_eq!(router.get_prefix(), "");
940        assert!(router.get_tags().is_empty());
941        assert!(router.get_dependencies().is_empty());
942        assert!(router.get_responses().is_empty());
943        assert_eq!(router.is_deprecated(), None);
944        assert!(router.get_include_in_schema());
945        assert!(router.get_routes().is_empty());
946    }
947
948    // =========================================================================
949    // IncludeConfig Tests
950    // =========================================================================
951
952    #[test]
953    fn test_include_config_default() {
954        let config = IncludeConfig::new();
955        assert!(config.get_prefix().is_none());
956        assert!(config.get_tags().is_empty());
957        assert!(config.get_dependencies().is_empty());
958        assert!(config.get_responses().is_empty());
959        assert!(config.get_deprecated().is_none());
960        assert!(config.get_include_in_schema().is_none());
961    }
962
963    #[test]
964    fn test_include_config_prefix() {
965        let config = IncludeConfig::new().prefix("/api/v1");
966        assert_eq!(config.get_prefix(), Some("/api/v1"));
967
968        // Should normalize prefix without leading slash
969        let config = IncludeConfig::new().prefix("api/v1");
970        assert_eq!(config.get_prefix(), Some("/api/v1"));
971
972        // Should remove trailing slash
973        let config = IncludeConfig::new().prefix("/api/v1/");
974        assert_eq!(config.get_prefix(), Some("/api/v1"));
975    }
976
977    #[test]
978    fn test_include_config_tags() {
979        let config = IncludeConfig::new().tags(vec!["api", "v1"]).tag("extra");
980        assert_eq!(config.get_tags(), &["api", "v1", "extra"]);
981    }
982
983    #[test]
984    fn test_include_config_dependencies() {
985        let dep1 = RouterDependency::new("auth", |_ctx, _req| async { Ok(()) });
986        let dep2 = RouterDependency::new("rate_limit", |_ctx, _req| async { Ok(()) });
987
988        let config = IncludeConfig::new().dependency(dep1).dependency(dep2);
989        assert_eq!(config.get_dependencies().len(), 2);
990    }
991
992    #[test]
993    fn test_include_config_responses() {
994        let config = IncludeConfig::new()
995            .response(401, ResponseDef::new("Unauthorized"))
996            .response(500, ResponseDef::new("Server Error"));
997        assert_eq!(config.get_responses().len(), 2);
998    }
999
1000    #[test]
1001    fn test_include_config_deprecated() {
1002        let config = IncludeConfig::new().deprecated(true);
1003        assert_eq!(config.get_deprecated(), Some(true));
1004
1005        let config = IncludeConfig::new().deprecated(false);
1006        assert_eq!(config.get_deprecated(), Some(false));
1007    }
1008
1009    #[test]
1010    fn test_include_config_include_in_schema() {
1011        let config = IncludeConfig::new().include_in_schema(false);
1012        assert_eq!(config.get_include_in_schema(), Some(false));
1013    }
1014
1015    // =========================================================================
1016    // Merge Rules Tests
1017    // =========================================================================
1018
1019    #[test]
1020    fn test_merge_rule_prefix_prepending() {
1021        // config.prefix + router.prefix should be combined
1022        let inner_router = APIRouter::new().prefix("/users");
1023        let config = IncludeConfig::new().prefix("/api/v1");
1024
1025        let outer = APIRouter::new().include_router_with_config(inner_router, config);
1026
1027        // Check that routes were merged (we can't directly access the full path,
1028        // but we can verify the outer router structure)
1029        // The inner router's routes should now have paths like /api/v1/users
1030        assert_eq!(outer.get_routes().len(), 0); // No routes were added to inner
1031    }
1032
1033    #[test]
1034    fn test_merge_rule_tags_prepending() {
1035        // config.tags + router.tags should be merged
1036        let inner = APIRouter::new().tags(vec!["users"]);
1037        let config = IncludeConfig::new().tags(vec!["api", "v1"]);
1038
1039        let outer = APIRouter::new()
1040            .tags(vec!["outer"])
1041            .include_router_with_config(inner, config);
1042
1043        // Outer keeps its own tags
1044        assert_eq!(outer.get_tags(), &["outer"]);
1045    }
1046
1047    #[test]
1048    fn test_merge_rule_deprecated_override() {
1049        // config.deprecated should override router.deprecated
1050        let inner = APIRouter::new().deprecated(false);
1051        let config = IncludeConfig::new().deprecated(true);
1052
1053        let outer = APIRouter::new().include_router_with_config(inner, config);
1054
1055        // The override happens at the route level, not router level
1056        assert_eq!(outer.is_deprecated(), None);
1057    }
1058
1059    #[test]
1060    fn test_merge_rule_include_in_schema_override() {
1061        // config.include_in_schema should override router.include_in_schema
1062        let inner = APIRouter::new().include_in_schema(true);
1063        let config = IncludeConfig::new().include_in_schema(false);
1064
1065        let _outer = APIRouter::new().include_router_with_config(inner, config);
1066        // Routes from inner should have include_in_schema = false
1067    }
1068
1069    #[test]
1070    fn test_merge_rule_responses_priority() {
1071        // route > router > config for responses
1072        let inner = APIRouter::new()
1073            .response(200, ResponseDef::new("Router Success"))
1074            .response(404, ResponseDef::new("Router Not Found"));
1075
1076        let config = IncludeConfig::new()
1077            .response(200, ResponseDef::new("Config Success"))
1078            .response(500, ResponseDef::new("Config Error"));
1079
1080        let outer = APIRouter::new().include_router_with_config(inner, config);
1081
1082        let responses = outer.get_responses();
1083        // Router's 200 should override config's 200
1084        assert_eq!(responses.get(&200).unwrap().description, "Router Success");
1085        // Config's 500 should be present
1086        assert_eq!(responses.get(&500).unwrap().description, "Config Error");
1087        // Router's 404 should be present
1088        assert_eq!(responses.get(&404).unwrap().description, "Router Not Found");
1089    }
1090
1091    #[test]
1092    fn test_recursive_router_inclusion() {
1093        // Routers can include other routers at multiple levels
1094        let level3 = APIRouter::new().prefix("/items");
1095        let level2 = APIRouter::new().prefix("/users").include_router(level3);
1096        let level1 = APIRouter::new().prefix("/api").include_router(level2);
1097
1098        // The final router should have prefix /api
1099        // Routes from level3 should be at /api/users/items
1100        assert_eq!(level1.get_prefix(), "/api");
1101    }
1102
1103    #[test]
1104    fn test_recursive_config_merging() {
1105        // Multi-level config merging
1106        let inner = APIRouter::new().tags(vec!["items"]);
1107        let middle_config = IncludeConfig::new().tags(vec!["users"]).prefix("/users");
1108        let outer_config = IncludeConfig::new().tags(vec!["api"]).prefix("/api");
1109
1110        let middle = APIRouter::new().include_router_with_config(inner, middle_config);
1111        let outer = APIRouter::new().include_router_with_config(middle, outer_config);
1112
1113        // Outer has its own (empty) tags, but routes should have merged tags
1114        assert!(outer.get_tags().is_empty());
1115    }
1116
1117    #[test]
1118    fn test_include_config_empty_prefix() {
1119        // Empty prefix in config should not affect router prefix
1120        let inner = APIRouter::new().prefix("/users");
1121        let config = IncludeConfig::new(); // No prefix set
1122
1123        let outer = APIRouter::new()
1124            .prefix("/api")
1125            .include_router_with_config(inner, config);
1126
1127        // Outer keeps its prefix
1128        assert_eq!(outer.get_prefix(), "/api");
1129    }
1130
1131    #[test]
1132    fn test_multi_level_path_construction() {
1133        // Test that paths are correctly constructed at multiple levels
1134        // /api + /v1 + /users + /{id} = /api/v1/users/{id}
1135
1136        // We can't directly test the path construction without adding routes,
1137        // but we can test the combine_paths function
1138        let level1 = "/api";
1139        let level2 = "/v1";
1140        let level3 = "/users";
1141        let level4 = "/{id}";
1142
1143        let combined_12 = combine_paths(level1, level2);
1144        assert_eq!(combined_12, "/api/v1");
1145
1146        let combined_123 = combine_paths(&combined_12, level3);
1147        assert_eq!(combined_123, "/api/v1/users");
1148
1149        let combined_1234 = combine_paths(&combined_123, level4);
1150        assert_eq!(combined_1234, "/api/v1/users/{id}");
1151    }
1152}