salvo_oapi/
routing.rs

1use std::any::TypeId;
2use std::collections::{BTreeSet, HashMap};
3use std::sync::{LazyLock, RwLock};
4
5use regex::Regex;
6use salvo_core::Router;
7
8use crate::{SecurityRequirement, path::PathItemType};
9
10#[derive(Debug, Default)]
11pub(crate) struct NormNode {
12    // pub(crate) router_id: usize,
13    pub(crate) handler_type_id: Option<TypeId>,
14    pub(crate) handler_type_name: Option<&'static str>,
15    pub(crate) method: Option<PathItemType>,
16    pub(crate) path: Option<String>,
17    pub(crate) children: Vec<NormNode>,
18    pub(crate) metadata: Metadata,
19}
20
21impl NormNode {
22    pub(crate) fn new(router: &Router, inherted_metadata: Metadata) -> Self {
23        let mut node = NormNode {
24            // router_id: router.id,
25            metadata: inherted_metadata,
26            ..NormNode::default()
27        };
28        let registry = METADATA_REGISTRY
29            .read()
30            .expect("failed to lock METADATA_REGISTRY for read");
31        if let Some(metadata) = registry.get(&router.id) {
32            node.metadata.tags.extend(metadata.tags.iter().cloned());
33            node.metadata
34                .securities
35                .extend(metadata.securities.iter().cloned());
36        }
37
38        let regex = Regex::new(r#"<([^/:>]+)(:[^>]*)?>"#).expect("invalid regex");
39        for filter in router.filters() {
40            let info = format!("{filter:?}");
41            if info.starts_with("path:") {
42                let path = info
43                    .split_once(':')
44                    .expect("split once by ':' should not be get `None`")
45                    .1;
46                node.path = Some(regex.replace_all(path, "{$1}").to_string());
47            } else if info.starts_with("method:") {
48                match info
49                    .split_once(':')
50                    .expect("split once by ':' should not be get `None`.")
51                    .1
52                {
53                    "GET" => node.method = Some(PathItemType::Get),
54                    "POST" => node.method = Some(PathItemType::Post),
55                    "PUT" => node.method = Some(PathItemType::Put),
56                    "DELETE" => node.method = Some(PathItemType::Delete),
57                    "HEAD" => node.method = Some(PathItemType::Head),
58                    "OPTIONS" => node.method = Some(PathItemType::Options),
59                    "CONNECT" => node.method = Some(PathItemType::Connect),
60                    "TRACE" => node.method = Some(PathItemType::Trace),
61                    "PATCH" => node.method = Some(PathItemType::Patch),
62                    _ => {}
63                }
64            }
65        }
66        node.handler_type_id = router.goal.as_ref().map(|h| h.type_id());
67        node.handler_type_name = router.goal.as_ref().map(|h| h.type_name());
68        let routers = router.routers();
69        if !routers.is_empty() {
70            for router in routers {
71                node.children
72                    .push(NormNode::new(router, node.metadata.clone()));
73            }
74        }
75        node
76    }
77}
78
79/// A component for save router metadata.
80type MetadataMap = RwLock<HashMap<usize, Metadata>>;
81static METADATA_REGISTRY: LazyLock<MetadataMap> = LazyLock::new(MetadataMap::default);
82
83/// Router extension trait for openapi metadata.
84pub trait RouterExt {
85    /// Add security requirement to the router.
86    ///
87    /// All endpoints in the router and it's descents will inherit this security requirement.
88    fn oapi_security(self, security: SecurityRequirement) -> Self;
89
90    /// Add security requirements to the router.
91    ///
92    /// All endpoints in the router and it's descents will inherit these security requirements.
93    fn oapi_securities<I>(self, security: I) -> Self
94    where
95        I: IntoIterator<Item = SecurityRequirement>;
96
97    /// Add tag to the router.
98    ///
99    /// All endpoints in the router and it's descents will inherit this tag.
100    fn oapi_tag(self, tag: impl Into<String>) -> Self;
101
102    /// Add tags to the router.
103    ///
104    /// All endpoints in the router and it's descents will inherit thes tags.
105    fn oapi_tags<I, V>(self, tags: I) -> Self
106    where
107        I: IntoIterator<Item = V>,
108        V: Into<String>;
109}
110
111impl RouterExt for Router {
112    fn oapi_security(self, security: SecurityRequirement) -> Self {
113        let mut guard = METADATA_REGISTRY
114            .write()
115            .expect("failed to lock METADATA_REGISTRY for write");
116        let metadata = guard.entry(self.id).or_default();
117        metadata.securities.push(security);
118        self
119    }
120    fn oapi_securities<I>(self, iter: I) -> Self
121    where
122        I: IntoIterator<Item = SecurityRequirement>,
123    {
124        let mut guard = METADATA_REGISTRY
125            .write()
126            .expect("failed to lock METADATA_REGISTRY for write");
127        let metadata = guard.entry(self.id).or_default();
128        metadata.securities.extend(iter);
129        self
130    }
131    fn oapi_tag(self, tag: impl Into<String>) -> Self {
132        let mut guard = METADATA_REGISTRY
133            .write()
134            .expect("failed to lock METADATA_REGISTRY for write");
135        let metadata = guard.entry(self.id).or_default();
136        metadata.tags.insert(tag.into());
137        self
138    }
139    fn oapi_tags<I, V>(self, iter: I) -> Self
140    where
141        I: IntoIterator<Item = V>,
142        V: Into<String>,
143    {
144        let mut guard = METADATA_REGISTRY
145            .write()
146            .expect("failed to lock METADATA_REGISTRY for write");
147        let metadata = guard.entry(self.id).or_default();
148        metadata.tags.extend(iter.into_iter().map(Into::into));
149        self
150    }
151}
152
153#[non_exhaustive]
154#[derive(Default, Clone, Debug)]
155pub(crate) struct Metadata {
156    pub(crate) tags: BTreeSet<String>,
157    pub(crate) securities: Vec<SecurityRequirement>,
158}