apimock/core/
app.rs

1use std::net::{SocketAddr, ToSocketAddrs};
2use std::path::Path;
3use std::sync::Arc;
4
5use console::style;
6use hyper::body::Bytes;
7use hyper::{body, service::service_fn, Request, Response};
8use hyper_util::{
9    rt::{TokioExecutor, TokioIo},
10    server::conn::auto::Builder,
11};
12use rhai::Engine;
13use std::convert::Infallible;
14use tokio::sync::mpsc::Sender;
15use tokio::{net::TcpListener, sync::Mutex};
16
17use super::app_state::{AppState, Middleware};
18use super::args::EnvArgs;
19use super::config::{Config, ConfigUrlPaths, ConfigUrlPathsJsonpathPatterns};
20use super::constant::APP_NAME;
21use super::logger::init_logger;
22use super::server::handle;
23
24type BoxBody = http_body_util::combinators::BoxBody<Bytes, Infallible>;
25
26/// app
27pub struct App {
28    addr: SocketAddr,
29    listener: TcpListener,
30    app_state: AppState,
31}
32
33impl App {
34    /// create new app
35    ///
36    /// - listener_port_to_overwrite: ignores port in config toml. used in both arguments and tests
37    pub async fn new(
38        env_args: EnvArgs,
39        spawn_tx: Option<Sender<String>>,
40        includes_ansi_codes: bool,
41    ) -> Self {
42        let _ = init_logger(spawn_tx, includes_ansi_codes);
43
44        let mut config = Config::new(env_args.config_filepath.as_ref());
45
46        if let Some(port) = env_args.port {
47            config.port = port;
48        }
49
50        let addr = config
51            .listener_address()
52            .to_socket_addrs()
53            .expect("invalid listend address or port")
54            .next()
55            .expect("failed to resolve address");
56        let listener = TcpListener::bind(addr)
57            .await
58            .expect("tcp listener failed to bind address");
59
60        let middleware = match env_args.middleware_filepath {
61            Some(middleware_filepath) if Path::new(middleware_filepath.as_str()).exists() => {
62                let engine = Engine::new();
63                // todo: watch source file change - `notify` crate ?
64                let ast = engine
65                    .compile_file(middleware_filepath.clone().into())
66                    .expect(
67                        format!(
68                            "failed to compile middleware file to get ast: {}",
69                            middleware_filepath
70                        )
71                        .as_str(),
72                    );
73
74                let middleware = Middleware {
75                    engine: Arc::new(engine),
76                    filepath: middleware_filepath.to_owned(),
77                    ast,
78                };
79
80                log::info!("\nMiddleware is activated: {}", middleware_filepath);
81                Some(middleware)
82            }
83            _ => None,
84        };
85        let app_state = AppState { config, middleware };
86
87        App {
88            addr,
89            listener,
90            app_state,
91        }
92    }
93
94    /// app start
95    pub async fn start(&self) {
96        log::info!(
97            "\nGreetings from {APP_NAME} !!\nListening on {} ...\n",
98            style(format!("http://{}", self.addr)).cyan()
99        );
100        let app_state = Arc::new(Mutex::new(self.app_state.clone()));
101        loop {
102            let (stream, _) = self
103                .listener
104                .accept()
105                .await
106                .expect("tcp listener failed to accept");
107            let io = TokioIo::new(stream);
108
109            let app_state = app_state.clone();
110            tokio::task::spawn(async move {
111                // Finally, we bind the incoming connection to our `hello` service
112                if let Err(err) = Builder::new(TokioExecutor::new())
113                    // `service_fn` converts our function in a `Service`
114                    .serve_connection(
115                        io,
116                        service_fn(move |req: Request<body::Incoming>| {
117                            service(req, app_state.clone())
118                        }),
119                    )
120                    .await
121                {
122                    log::error!("error serving connection: {:?}", err);
123                }
124            });
125        }
126    }
127
128    /// get [url.paths] in config
129    pub fn config_url_paths(&self) -> Option<ConfigUrlPaths> {
130        self.app_state.config.paths.clone()
131    }
132
133    /// get [url.paths_patterns] in config
134    pub fn config_url_paths_patterns(&self) -> Option<ConfigUrlPathsJsonpathPatterns> {
135        self.app_state.config.paths_jsonpath_patterns.clone()
136    }
137}
138
139/// handle http service
140async fn service(
141    req: Request<body::Incoming>,
142    app_state: Arc<Mutex<AppState>>,
143) -> Result<Response<BoxBody>, hyper::http::Error> {
144    handle(req, Arc::clone(&app_state)).await
145}