awesome_operates/router/
mod.rs

1use axum::{
2    body::Body,
3    http::{self, Method, Request},
4    response::Response,
5    routing::MethodRouter,
6    Router,
7};
8use serde_json::Value;
9use snafu::OptionExt;
10use tower::{Service, ServiceExt};
11
12use crate::error::{OptionNoneSnafu, Result};
13use crate::helper::iter_object;
14use crate::method_exchange;
15
16mod handler;
17#[cfg(test)]
18mod tests;
19
20/// if used as static, use `tokio::sync::Mutex`, this is very importment
21/// ```rust,no_run
22/// use std::sync::Arc;
23///
24/// use axum::http::Method;
25/// use once_cell::sync::Lazy;
26/// use tokio::sync::Mutex;
27/// use serde_json::Value;
28///
29/// use awesome_operates::router::RequestMatcher;
30///
31/// static REQUEST_MATCHER: Lazy<Arc<Mutex<RequestMatcher>>> = Lazy::new(||
32/// { Arc::new(Mutex::new(RequestMatcher::default())) });
33///
34/// #[tokio::test]
35/// async fn matcher() {
36///     let api = tokio::fs::read_to_string("api.json").await.unwrap();
37///     let body = serde_json::from_str::<Value>(&api).unwrap();
38///
39///     let mut request_matcher = RequestMatcher::from_openapi(&body, "").unwrap();
40///     // use directly
41///     request_matcher.match_request_to_response(Method::GET, "/api/test", None).await.unwrap();
42///
43///     // or use global
44///     *REQUEST_MATCHER.lock().await = request_matcher;
45///     REQUEST_MATCHER.lock().await.match_request_to_response(Method::GET, "/api/test", None).await.unwrap();
46/// }
47/// ```
48#[derive(Default)]
49pub struct RequestMatcher {
50    pub router: Router,
51}
52
53impl RequestMatcher {
54    pub fn from_openapi(openapi: &Value, path_prefix: &str) -> Result<Self> {
55        let route_handles = Self::openapi_route_handles(openapi, path_prefix)?;
56        Ok(RequestMatcher::from_route_methods(route_handles))
57    }
58
59    pub fn from_route_methods(route_methods: Vec<(String, MethodRouter)>) -> Self {
60        let mut router = Router::new();
61        for (path, resp) in route_methods {
62            router = router.route(path.as_ref(), resp);
63        }
64        RequestMatcher { router }
65    }
66
67    /// path_prefix is like "/sys-layer", "/api/v1"
68    /// openapi refer to `src/test_files/openapi.json`
69    pub fn openapi_route_handles(
70        openapi: &Value,
71        path_prefix: &str,
72    ) -> Result<Vec<(String, MethodRouter)>> {
73        let mut route_handlers = vec![];
74        for (path, operate) in iter_object(openapi, "paths")? {
75            let path = path.replace('{', ":").replace('}', "");
76            for (method, detail) in operate.as_object().context(OptionNoneSnafu)?.iter() {
77                if !detail.is_object() {
78                    continue;
79                }
80                let summary = detail.get("summary");
81                let component = Self::api_component(
82                    openapi,
83                    detail.pointer("/requestBody/content/application~1json/schema/$ref"),
84                );
85                let path_with_prefix = format!("{}{path}", path_prefix.trim_end_matches('/'));
86                let resp = serde_json::json!({
87                    "openapi_path": path,
88                    "method": method,
89                    "summary": summary,
90                    "component": component,
91                    "path_with_prefix": path_with_prefix,
92                });
93                tracing::debug!(
94                    r#"read route handle
95                    path_prefix[{path_prefix}]
96                    path[{path}]
97                    method[{method}]
98                    summary[{summary:?}]
99                    component[{component:?}]
100                    resp[{resp}]"#
101                );
102                route_handlers.push((path_with_prefix, method_exchange!(method, &path, resp)));
103            }
104        }
105        Ok(route_handlers)
106    }
107
108    pub fn api_component<'a>(
109        openapi: &'a Value,
110        component_path: Option<&Value>,
111    ) -> Option<&'a Value> {
112        if let Some(path) = component_path {
113            if let Some(p) = path.as_str() {
114                if p.starts_with('#') {
115                    return openapi.pointer(&p.replace('#', ""));
116                }
117            }
118        }
119        None
120    }
121
122    pub async fn match_request_to_response(
123        &mut self,
124        method: Method,
125        path: &str,
126        body: Option<Body>,
127    ) -> anyhow::Result<Response> {
128        // this line is very important
129        let method = method.as_str().to_uppercase().parse().unwrap();
130        tracing::debug!(
131            "match request [method]{} [path]:{} body:[{body:?}] ",
132            method,
133            path
134        );
135        let request = Self::build_request(method, path, body);
136        tracing::debug!("match request before {request:?}");
137        let response = ServiceExt::<Request<Body>>::ready(&mut self.router)
138            .await?
139            .call(request)
140            .await?;
141        tracing::debug!("match api with result status: {}", response.status());
142        Ok(response)
143    }
144
145    pub async fn match_request_to_json_response(
146        &mut self,
147        method: Method,
148        path: &str,
149        body: Option<Body>,
150    ) -> anyhow::Result<Value> {
151        let response = self.match_request_to_response(method, path, body).await?;
152        let bytes = http_body_util::BodyExt::collect(response.into_body())
153            .await?
154            .to_bytes();
155        Ok(serde_json::from_slice(&bytes)?)
156    }
157
158    pub fn build_request(method: Method, path: &str, body: Option<Body>) -> Request<Body> {
159        let body = body.unwrap_or_default();
160        Request::builder()
161            .method(method)
162            .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
163            .uri(path)
164            .body(body)
165            .unwrap()
166    }
167}