rapid_web/
server.rs

1use super::{
2	actix::{
3		dev::{ServiceRequest, ServiceResponse},
4		middleware::{Condition, NormalizePath},
5		App, Error, HttpServer, Scope,
6	},
7	cors::Cors,
8	default_routes::static_files,
9	logger::RapidLogger,
10	shift::generate::create_typescript_types,
11	tui::server_init,
12	util::{
13		check_for_invalid_handlers, get_bindings_directory, get_routes_dir, get_server_port, is_logging, is_serving_static_files,
14		should_generate_types, NEXTJS_ROUTE_PATH, REMIX_ROUTE_PATH,
15	},
16};
17use crate::{logger::init_logger, tui::rapid_chevrons};
18use actix_http::{body::MessageBody, Request, Response};
19use actix_service::{IntoServiceFactory, ServiceFactory};
20use actix_web::dev::AppConfig;
21use colorful::{Color, Colorful};
22use lazy_static::lazy_static;
23use rapid_cli::rapid_config::config::{find_rapid_config, RapidConfig};
24use spinach::Spinach;
25use std::{
26	env::current_dir,
27	path::PathBuf,
28	thread,
29	time::{self, Instant},
30};
31extern crate proc_macro;
32
33#[derive(Clone)]
34pub struct RapidServer {
35	pub port: Option<u16>,
36	pub hostname: Option<String>,
37}
38
39// A lazily evaluated const variable for accessing the rapidConfig at runtime
40lazy_static! {
41	pub static ref RAPID_SERVER_CONFIG: RapidConfig = find_rapid_config();
42}
43
44/// A custom actix-web server implementation for the Rapid Framework
45/// # Examples
46/// ```
47/// use rapid_web::server::RapidServer;
48///
49/// let app = RapidServer::create(None, None, None, None);
50///
51/// app.listen(HttpServer::new(move || {
52///		RapidServer::router(None).route("/", web::get().to(route))
53/// })).await
54///
55/// ```
56impl RapidServer {
57	pub fn create(port: Option<u16>, hostname: Option<String>) -> Self {
58		Self { port, hostname }
59	}
60
61	/// A stock re-export of the actix-web "App::new()" router with a few extras
62	/// This router does not support type-safe file based routing
63	/// Note: to experience the full capabilities of rapid-web, we recommend using the RapidServer::fs_router function
64	pub fn router(
65		cors: Option<Cors>,
66		log_type: Option<RapidLogger>,
67	) -> App<impl ServiceFactory<ServiceRequest, Response = ServiceResponse<impl MessageBody>, Config = (), InitError = (), Error = Error>> {
68		// First we need to go to the rapid config file and check for the is_logging variable
69		let is_logging = is_logging();
70
71		// Check if we should also be serving static files
72		let is_serving_static_files = is_serving_static_files();
73
74		let config_logging_server = {
75			if is_logging {
76				match log_type {
77					Some(logging_type) => App::new()
78						.wrap(cors.unwrap_or(Cors::default()))
79						.wrap(Condition::new(true, logging_type))
80						.wrap(NormalizePath::trim()),
81					None => App::new()
82						.wrap(cors.unwrap_or(Cors::default()))
83						.wrap(Condition::new(true, RapidLogger::minimal()))
84						.wrap(NormalizePath::trim()),
85				}
86			} else {
87				App::new()
88					.wrap(cors.unwrap_or(Cors::default()))
89					.wrap(Condition::new(false, RapidLogger::minimal()))
90					.wrap(NormalizePath::trim())
91			}
92		};
93
94		// Depending on what is inside of the config file lets attempt to serve static files..
95		match is_serving_static_files {
96			true => config_logging_server.service(static_files::static_files()),
97			false => config_logging_server,
98		}
99	}
100
101	/// A file-system based router for rapid-web
102	///
103	/// Build your api with a simple file based technique (ex: _middleware.rs, index.rs)
104	///
105	/// * `routes` - A string slice that holds the path to the file system routes root directory (ex: "src/routes") -- this value can be anything as long as it is a valid (relative) directory path
106	/// * `cors` - An optional cors value that can be used to configure the cors middleware
107	/// * `log_type` - An optional logger type that can be used to configure the logger middleware used for every request/response
108	///
109	/// > Docs coming soon...
110	pub fn fs_router(
111		cors: Option<Cors>,
112		log_type: Option<RapidLogger>,
113		routes: Scope,
114	) -> App<impl ServiceFactory<ServiceRequest, Response = ServiceResponse<impl MessageBody>, Config = (), InitError = (), Error = Error>> {
115		// Initialize our router with the config options the user passed in
116		RapidServer::router(cors, log_type).service(routes)
117	}
118
119	/// Takes in a pre-configured HttpServer and listens on the specified port(s)
120	///
121	/// # Notes
122	/// This function will try to initalize a logger in case one has not already been initalized.
123	/// If you would like to use your own logger, make sure it has been initalized before this
124	/// function is called
125	pub async fn listen<F, I, S, B>(self, server: HttpServer<F, I, S, B>) -> std::io::Result<()>
126	where
127		F: Fn() -> I + Send + Clone + 'static,
128		I: IntoServiceFactory<S, Request>,
129		S: ServiceFactory<Request, Config = AppConfig> + 'static,
130		S::Error: Into<Error>,
131		S::InitError: std::fmt::Debug,
132		S::Response: Into<Response<B>>,
133		B: MessageBody + 'static,
134	{
135		// Initialize the env_logger for rapid server logs
136		init_logger();
137
138		// Grab the users configured server binding values from either the RapidServer object
139		// or the actualy rapid config file in the project root
140		let bind_config = get_default_bind_config(RAPID_SERVER_CONFIG.clone(), self.hostname, self.port);
141
142		// Grab the routes directory from the rapid config file (this powers how we export types)
143		// Note: make sure we panic if we are not able to detect it
144		let routes_dir = match RAPID_SERVER_CONFIG.app_type.as_str() {
145			"server" => get_routes_dir(RAPID_SERVER_CONFIG.server.as_ref()),
146			"remix" => REMIX_ROUTE_PATH.to_owned(),
147			_ => NEXTJS_ROUTE_PATH.to_owned(),
148		};
149
150		// Grab the bindings directory from the rapid config file
151		// We want to make sure that it is valid and is actually defined (it defaults to an Option<String>)
152		// TODO: this needs refactored (or abstracted) really bad -- we need to have the match statements if we want better panic messages
153		let bindings_out_dir = get_bindings_directory();
154
155		// Check if we should generate typescript types or not
156		let should_generate_typescript_types = should_generate_types(RAPID_SERVER_CONFIG.clone());
157
158		// Show the server initialization message
159		server_init(bind_config.clone());
160
161		// Only trigger type generation if the users configured options in their rapid config file permits it
162		// (we also dont want to generate types in a production environment)
163		if should_generate_typescript_types && cfg!(debug_assertions) {
164			generate_typescript_types(bindings_out_dir, routes_dir.clone(), RAPID_SERVER_CONFIG.clone());
165		}
166
167		// Check for any invalid routes and log them to the console
168		check_for_invalid_handlers(&routes_dir);
169
170		// Finally, bind and run the rapid server
171		server.bind(bind_config)?.run().await
172	}
173}
174
175/// Generate the default server bind config based on the rapid config file
176fn get_default_bind_config(config: RapidConfig, host_name: Option<String>, port: Option<u16>) -> (String, u16) {
177	// Get the hostname from the server object initialized by the consumer
178	// We need to check if they passed one in -- if they didn't, we'll use localhost
179	let server_hostname = match host_name {
180		Some(value) => value,
181		None => String::from("localhost"),
182	};
183
184	// Grab a fallback port if the user did not specify one inside of the root rapid config file
185	let fallback_port = match port {
186		Some(value) => value,
187		None => 8080,
188	};
189
190	// Grab the port from either the server config or the rapid config file
191	let port = get_server_port(config, fallback_port);
192
193	(server_hostname, port)
194}
195
196pub fn generate_typescript_types(bindings_out_dir: PathBuf, routes_dir: String, config: RapidConfig) {
197	// Check if we should be converting types inside of every directory
198	let every_dir_types_gen = match config.app_type.as_str() {
199		"server" => match config.server {
200			Some(server) => match server.typescript_generation_directory {
201				Some(value) => value,
202				None => "".to_string(),
203			},
204			None => "".to_string(),
205		},
206		"remix" => match config.remix {
207			Some(remix) => match remix.typescript_generation_directory {
208				Some(value) => value,
209				None => "".to_string(),
210			},
211			None => "".to_string(),
212		},
213		_ => match config.nextjs {
214			Some(nextjs) => match nextjs.typescript_generation_directory {
215				Some(value) => value,
216				None => "".to_string(),
217			},
218			None => "".to_string(),
219		},
220	};
221
222	let routes_directory = current_dir()
223		.expect("Could not parse routes direcory path found in rapid config file.")
224		.join(PathBuf::from(routes_dir.clone()));
225
226	// Generate our type gen dir
227	let type_generation_directory = if every_dir_types_gen != "" {
228		current_dir()
229			.expect("Could not parse current directory while executing type generation!")
230			.join(every_dir_types_gen)
231	} else {
232		// If the typegen directory was not defined by the user, simply fallback to only doing handler types in the routes directorys
233		routes_directory.clone()
234	};
235
236	let start_time = Instant::now();
237	// Show a loading spinner as needed
238	let loading = Spinach::new(format!("{} Generating types...", rapid_chevrons()));
239	// TODO: Support output types with this function
240	create_typescript_types(bindings_out_dir, routes_directory, type_generation_directory);
241
242	// Sleep a little to show loading animation
243	let timeout = time::Duration::from_millis(550);
244	thread::sleep(timeout);
245
246	loading.succeed(format!(
247		"Generated typescript types in {} ms\n",
248		start_time.elapsed().as_millis().to_string().color(Color::Blue).bold()
249	));
250}
251
252#[cfg(test)]
253mod tests {
254	use super::*;
255	use actix_web::{http::header::ContentType, test, web};
256	use std::fs::File;
257	use std::io::prelude::*;
258
259	#[actix_web::test]
260	async fn test_server_and_router() {
261		//let app = RapidServer::create(None, None);
262
263		let rapid_config_test = r#"app_type = "server"
264
265		[server]
266		serve_static_files = true
267		is_logging = true
268		typescript_generation = true
269		port = 8080
270		routes_directory = "src/routes"
271		bindings_export_path = "/"
272		"#;
273
274		let mut rapid_config = File::create("rapid.toml").unwrap();
275
276		rapid_config.write_all(rapid_config_test.as_bytes()).unwrap();
277
278		let app = test::init_service(RapidServer::router(None, None).route("/", web::get().to(|| async { "Hello World!" }))).await;
279
280		let req = test::TestRequest::default().insert_header(ContentType::plaintext()).to_request();
281		let resp = test::call_service(&app, req).await;
282		assert!(resp.status().is_success());
283
284		std::fs::remove_file("rapid.toml").unwrap();
285	}
286}