libwizard/
lib.rs

1pub mod prelude;
2pub mod rust_to_html;
3use owo_colors::OwoColorize;
4use std::{
5    fs::{self, File},
6    io::{Read, Write},
7    net::{TcpListener, TcpStream},
8    path::Path,
9    thread,
10};
11
12pub trait Debug {
13    fn debug(&self);
14}
15
16pub struct Server {
17    host: String,
18    port: u32,
19}
20
21pub struct ServerResponse {
22    content: String,
23}
24pub struct StyleResponse {
25    content: String,
26}
27pub struct Custom404 {
28    content: String,
29}
30#[derive(Clone)]
31pub struct CustomRoutes {
32    pub path: String,
33    pub content_type: String,
34    pub response: String,
35    pub styles: Option<String>,
36}
37impl Custom404 {
38    pub fn new(content: impl Into<String>) -> Self {
39        Self {
40            content: content.into(),
41        }
42    }
43    pub fn default() -> Self {
44        Self {
45            content: "404 page not found!".to_string(),
46        }
47    }
48}
49
50impl CustomRoutes {
51    pub fn new(
52        path: impl Into<String>,
53        content_type: impl Into<String>,
54        response: impl Into<String>,
55        css_file: Option<impl Into<String>>,
56    ) -> Self {
57        Self {
58            path: path.into(),
59            content_type: content_type.into(),
60            response: response.into(),
61            styles: css_file.map(Into::into),
62        }
63    }
64
65    pub fn include_css(&self) -> Option<String> {
66        if let Some(styles) = &self.styles {
67            match fs::read_to_string(styles) {
68                Ok(css) => Some(css),
69                Err(e) => {
70                    eprintln!("Error reading CSS file: {}", e);
71                    None
72                }
73            }
74        } else {
75            None
76        }
77    }
78}
79impl Server {
80    pub fn new(host: impl Into<String>, port: u32) -> Self {
81        Self {
82            host: host.into(),
83            port,
84        }
85    }
86
87    pub fn start(
88        &self,
89        content: ServerResponse,
90        style: StyleResponse,
91        custom_routes: Vec<CustomRoutes>,
92        custom_404: Custom404,
93    ) {
94        let custom404 = custom_404.content.clone();
95        let routes = custom_routes.clone();
96        fn handle_client(
97            mut stream: TcpStream,
98            content: String,
99            style: String,
100            custom_routes: Vec<CustomRoutes>,
101            custom_404: String,
102        ) {
103            let mut buffer = [0; 1024];
104            let css_content = include_css(&style);
105            match stream.read(&mut buffer) {
106                Ok(_) => {
107                    let request = String::from_utf8_lossy(&buffer);
108
109                    let response = if request.starts_with("GET / HTTP/1.1") {
110                        // Handle the request for the root path (index.html)
111                        format!(
112                            "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n{}",
113                            content
114                        )
115                    } else if request.starts_with("GET /style.css HTTP/1.1") {
116                        format!(
117                            "HTTP/1.1 200 OK\r\nContent-Type: text/css\r\n\r\n{}",
118                            css_content
119                        )
120                    } else {
121                        // Check for custom routes
122                        for route in custom_routes.iter() {
123                            if request.starts_with(&format!("GET {} HTTP/1.1", route.path)) {
124                                let response = match &route.content_type[..] {
125                                    "text/plain" => format!(
126                                        "HTTP/1.1 200 OK\r\nContent-Type: {}\r\n\r\n{}",
127                                        route.content_type, route.response
128                                    ),
129                                    "application/json" => format!(
130                                        "HTTP/1.1 200 OK\r\nContent-Type: {}\r\n\r\n{}",
131                                        route.content_type, route.response
132                                    ),
133                                    _ => format!(
134                                        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n{}",
135                                        route.response
136                                    ),
137                                };
138                                // Check if the custom route has a CSS file
139                                if let Some(css_content) = &route.styles {
140                                    // Inject the CSS content into the response using a <style> tag
141                                    let styled_response = format!(
142                                        "{}\r\n\r\n<style type=\"text/css\">\r\n{}\r\n</style>",
143                                        response, css_content
144                                    );
145                                    return stream.write_all(styled_response.as_bytes()).unwrap();
146                                } else {
147                                    return stream.write_all(response.as_bytes()).unwrap();
148                                }
149                            }
150                        }
151
152                        // Handle other requests (404 Not Found)
153                        format!("HTTP/1.1 404 NOT FOUND\r\n\r\n{}", custom_404)
154                    };
155                    stream.write_all(response.as_bytes()).unwrap();
156                }
157                Err(e) => eprintln!("Error reading from connection: {}", e),
158            }
159        }
160
161        let listener = TcpListener::bind(format!("{}:{}", self.host, self.port))
162            .expect("Failed to bind adress!");
163        println!("Server listening on http://{}:{}", self.host, self.port);
164
165        thread::spawn({
166            move || {
167                for stream in listener.incoming() {
168                    match stream {
169                        Ok(stream) => {
170                            let content = content.content.clone();
171                            let css_content = style.content.clone();
172                            let custom_routes = custom_routes.clone();
173                            let custom404 = custom404.clone();
174                            thread::spawn(move || {
175                                handle_client(
176                                    stream,
177                                    content,
178                                    css_content,
179                                    custom_routes,
180                                    custom404,
181                                );
182                            });
183                        }
184                        Err(e) => {
185                            println!("Error accepting connection: {}", e);
186                        }
187                    }
188                }
189            }
190        });
191        loop {
192            let mut prompt = String::new();
193            std::io::stdin().read_line(&mut prompt).unwrap_or_default();
194
195            match prompt.trim() {
196                ".help" => {
197                    println!("{}", "Commands:".green().bold());
198                    println!("{}", ".stop - Stop the server".yellow());
199                    println!("{}", ".info - Get server info".yellow());
200                }
201                ".stop" => {
202                    println!("Shutting down");
203                    std::process::exit(0);
204                }
205                ".routes" => {
206                    for route in routes.iter() {
207                        println!(
208                            "{} - {}",
209                            route.path.bold().green(),
210                            route.content_type.bold().yellow()
211                        );
212                    }
213                }
214                ".info" => {
215                    println!("Server listening on http://{}:{}", self.host, self.port);
216                }
217                _ => println!(
218                    "{}{}",
219                    "Unknown Command: ".red().bold(),
220                    prompt.trim().red()
221                ),
222            }
223        }
224    }
225}
226fn include_css(style_path: &str) -> String {
227    match fs::read_to_string(style_path) {
228        Ok(css) => css,
229        Err(e) => {
230            eprintln!("Error reading CSS file: {}", e);
231            "Error reading CSS file".to_string()
232        }
233    }
234}
235
236impl Debug for Server {
237    fn debug(&self) {
238        println!("Server {{ host: {}, port: {} }}", self.host, self.port)
239    }
240}
241impl ServerResponse {
242    pub fn new(content: impl Into<String>) -> Self {
243        Self {
244            content: content.into(),
245        }
246    }
247}
248impl StyleResponse {
249    pub fn new(content: impl Into<String>) -> Self {
250        Self {
251            content: content.into(),
252        }
253    }
254}
255pub fn include_html(path: impl Into<String> + std::convert::AsRef<std::path::Path>) -> String {
256    let binding = path.into();
257    let pth = Path::new(&binding);
258    let cat = File::open(pth);
259    match cat {
260        Ok(mut file) => {
261            let mut file_contents = String::new();
262            if let Err(error) = file.read_to_string(&mut file_contents) {
263                eprintln!("Error reading file: {}", error);
264            }
265
266            return file_contents;
267        }
268        Err(e) => {
269            eprintln!("Failed to read file {:?}", e);
270            return format!(
271                "
272                <div style='text-align: center; font-family: sans-serif;'>
273                    <h1 style='margin: auto; background: red; border-radius: 5px; padding: 5px; color: white;'>Error reading your file!</h1>
274                    <h2>Cannot find {}<br></h2>
275                    <p>{}</p>
276                </div>",
277                &binding, e,
278            );
279        }
280    }
281}