diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 748377a..ec4adc8 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1 +1,2 @@ pub mod runner; +mod test; \ No newline at end of file diff --git a/src/engine/test.rs b/src/engine/test.rs new file mode 100644 index 0000000..3ba3907 --- /dev/null +++ b/src/engine/test.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::engine::runner::run; + + #[test] + fn test_example_todo() { + run(&Path::new("examples/todo"), false); + } +} \ No newline at end of file diff --git a/src/file/parser.rs b/src/file/parser.rs index b79a1c4..d68c5a0 100644 --- a/src/file/parser.rs +++ b/src/file/parser.rs @@ -1,15 +1,16 @@ -use std::{path::PathBuf, io::{BufReader, Read, Seek, SeekFrom}}; +use std::{path::PathBuf, io::{BufReader, Read}}; use crate::{file::webx::WebXFile, reporting::error::{exit_error, ERROR_PARSE_IO, ERROR_SYNTAX, exit_error_unexpected_char, exit_error_unexpected, exit_error_expected_any_of_but_found, exit_error_expected_but_found}}; -use super::webx::{WebXScope, WebXModel}; +use super::webx::{WebXScope, WebXModel, WebXRouteMethod, WebXRoute}; struct WebXFileParser<'a> { file: &'a PathBuf, - content: &'a String, + _content: &'a String, reader: BufReader<&'a [u8]>, line: usize, column: usize, - index: u64, + peeked_index: u64, // "next index" + next_index: u64, // "current index" peeked: Option, } @@ -17,11 +18,12 @@ impl<'a> WebXFileParser<'a> { fn new(file: &'a PathBuf, content: &'a String) -> WebXFileParser<'a> { let mut p = WebXFileParser { file, - content, + _content: content, reader: BufReader::new(content.as_bytes()), line: 0, column: 0, - index: 0, + peeked_index: 0, + next_index: 0, peeked: None, }; p.peeked = p.__raw_next(); @@ -41,7 +43,9 @@ impl<'a> WebXFileParser<'a> { }; if bytes_read == 0 { return None; } let c = buf[0] as char; - self.index += 1; + self.peeked_index += 1; + // Index of the character returned by the next call to `next`. + self.next_index = self.peeked_index - 1; if c == '\n' { self.line += 1; self.column = 0; @@ -57,6 +61,15 @@ impl<'a> WebXFileParser<'a> { self.peeked = self.__raw_next(); c } + fn expect(&mut self) -> char { + let nc = self.next(); + self.expect_not_eof(nc) + } + + fn expect_not_eof(&mut self, nc: Option) -> char { + if nc.is_none() { exit_error_unexpected("EOF".to_string(), self.line, self.column, ERROR_SYNTAX); } + nc.unwrap() + } /// Expect a specific character to be next in the file. /// Increments the line and column counters. @@ -64,10 +77,9 @@ impl<'a> WebXFileParser<'a> { /// # Errors /// If EOF is reached, or the next character is not the expected one, an error is returned and the program exits. fn expect_specific(&mut self, nc: Option, expected: char) { - if nc.is_none() { - exit_error_unexpected("EOF".to_string(), self.line, self.column, ERROR_SYNTAX); - } else if nc.unwrap() != expected { - exit_error_expected_but_found(expected.to_string(), nc.unwrap().to_string(), self.line, self.column, ERROR_SYNTAX); + let nc = self.expect_not_eof(nc); + if nc != expected { + exit_error_expected_but_found(expected.to_string(), nc.to_string(), self.line, self.column, ERROR_SYNTAX); } } @@ -76,10 +88,11 @@ impl<'a> WebXFileParser<'a> { self.expect_specific(nc, expected); } - fn expect_specific_str(&mut self, expected: &str) { - for c in expected.chars() { - let nc = self.next(); - self.expect_specific(nc, c); + fn expect_specific_str(&mut self, expected: &str, already_read: usize) { + for c in expected.chars().skip(already_read) { + if self.expect() != c { + exit_error_expected_but_found(expected.to_string(), c.to_string(), self.line, self.column, ERROR_SYNTAX); + } } } @@ -89,10 +102,7 @@ impl<'a> WebXFileParser<'a> { /// # Errors /// If EOF is reached, or the next character is not one of the expected ones, an error is returned and the program exits. fn expect_any_of(&mut self, nc: Option, cs: Vec) -> char { - if nc.is_none() { - exit_error_unexpected("EOF".to_string(), self.line, self.column, ERROR_SYNTAX); - } - let nc = nc.unwrap(); + let nc = self.expect_not_eof(nc); if !cs.contains(&nc) { exit_error_expected_any_of_but_found(format!("{:?}", cs), nc, self.line, self.column, ERROR_SYNTAX); } @@ -115,13 +125,32 @@ impl<'a> WebXFileParser<'a> { None } + fn read_until_any_of(&mut self, cs: Vec) -> String { + let mut s = String::new(); + loop { + let nc = self.next(); + if nc.is_none() { break; } + let nc = nc.unwrap(); + if cs.contains(&nc) { break; } + s.push(nc); + } + s + } + fn read_until(&mut self, c: char) -> String { + self.read_until_any_of(vec![c]) + } + + fn parse_block(&mut self, start: char, end: char) -> String { let mut s = String::new(); + let mut depth = 1; loop { let nc = self.next(); if nc.is_none() { break; } let nc = nc.unwrap(); - if nc == c { break; } + if nc == start { depth += 1; } + else if nc == end { depth -= 1; } + if depth == 0 { break; } s.push(nc); } s @@ -170,7 +199,7 @@ impl<'a> WebXFileParser<'a> { /// include "path/to/file.webx"; /// ``` fn parse_include(&mut self) -> String { - self.expect_specific_str("nclude"); + self.expect_specific_str("include", 1); self.expect_next_specific('"'); let path = self.parse_string(); let nc = self.next_skip_whitespace(false); @@ -179,14 +208,14 @@ impl<'a> WebXFileParser<'a> { } fn parse_location(&mut self) -> Result { - self.expect_specific_str("ocation"); + self.expect_specific_str("location", 1); let nc = self.next_skip_whitespace(false); self.expect_specific(nc, '{'); self.parse_scope(false) } fn parse_model(&mut self) -> WebXModel { - self.expect_specific_str("odel"); + self.expect_specific_str("model", 1); let name = self.read_until('{'); let fields = self.read_until('}'); WebXModel { name, fields } @@ -196,12 +225,119 @@ impl<'a> WebXFileParser<'a> { todo!("parse_handler") } - fn parse_route(&mut self) { - todo!("parse_route") + /// Parse a URL path variable segment. + /// **Obs: This function does not parse the opening parenthesis.** + /// + /// ## Example: + /// ```ignore + /// (arg: string)? + /// ``` + fn parse_url_path_variable(&mut self) -> String { + let name = self.read_until(':').trim().to_string(); + let type_ = self.read_until(')').trim().to_string(); + format!("({}:{})", name, type_) } - fn parse_type(&mut self) { - todo!("parse_type") + /// Parse a URL path. + /// ## Supporting syntax: + /// - Static path segments + /// - Dynamic path segments (arguments) + /// - Optional path segments + /// - Wildcard path segments + /// - Regex path segments + /// + /// ## Example: + /// ```ignore + /// /path/to/(arg: string)?/* + /// ``` + fn parse_url_path(&mut self) -> String { + let mut path = String::new(); + let c = self.next_skip_whitespace(true); + if c.is_none() { exit_error_expected_but_found("endpoint path".to_string(), "EOF".to_string(), self.line, self.column, ERROR_SYNTAX); } + let mut c = c.unwrap(); + loop { + match c { + '(' => path.push_str(&self.parse_url_path_variable()), + c if c.is_alphanumeric() + || c == '/' + || c == '_' + || c == '*' + => path.push(c), + _ => break, + } + c = self.next().unwrap(); + } + path + } + + /// Parse a request body format. + /// ## Supporting syntax: + /// - pre-defined formats (json, form, text, html) + /// - (: , : , ...) + /// - user-defined model name + /// - + /// + /// ## Example: + /// ```ignore + /// json(text: string, n: number) + /// form(name: string, age: number) + /// User + /// ``` + fn parse_body_format(&mut self) -> Option { + match self.peek().unwrap() { + c if c.is_alphabetic() => self.parse_body_format(), + _ => None, + } + } + + fn parse_handler_calls(&mut self) -> Vec { + todo!("parse_handler_calls") + } + + fn parse_route_handlers(&mut self) -> Vec { + match self.next_skip_whitespace(true) { + Some('-') => { + self.expect_specific_str("->", 1); + self.parse_handler_calls() + }, + _ => vec![] + } + } + + fn parse_route_body(&mut self) -> Option { + match self.next_skip_whitespace(true) { + Some('{') => Some(self.parse_block('{', '}')), + Some('(') => Some(self.parse_block('(', ')')), + _ => None, + } + } + + /// Parse a route statement. + /// ## Supporting syntax: + /// - HTTP method (get, post, put, patch, delete, connect, options, trace, head) + /// - URL path with arguments + /// - Request body format (json, form, text, html, or user-defined model) + /// - Pre and post handlers + /// - Response body + /// - TypeScript code (TS): Using `{}` delimiters + /// - HTML template (TSX): Using `()` delimiters + /// + /// ## Example: + /// ```ignore + /// get /path/to/route (

My page

) + /// post /path/to/(arg: string)/route json(text: string, n: number) -> handler(arg, text) { + /// // ... + /// } + /// ``` + fn parse_route(&mut self, method: WebXRouteMethod) -> Result { + Ok(WebXRoute { + method, + path: self.parse_url_path(), + body_format: self.parse_body_format(), + pre_handlers: self.parse_route_handlers(), + body: self.parse_route_body(), + post_handlers: self.parse_route_handlers(), + }) } /// Parse either the global module scope, or a location scope. @@ -239,9 +375,56 @@ impl<'a> WebXFileParser<'a> { 'i' => scope.includes.push(self.parse_include()), 'l' => scope.scopes.push(self.parse_location()?), 'm' => scope.models.push(self.parse_model()), - 'h' => self.parse_handler(), - 'r' => self.parse_route(), - 't' => self.parse_type(), + 'h' => match self.expect() { + 'a' => self.parse_handler(), + 'e' => { + self.expect_specific_str("head", 2); + scope.routes.push(self.parse_route(WebXRouteMethod::HEAD)?); + }, + c => exit_error_expected_any_of_but_found("handler or head".to_string(), c, self.line, self.column, ERROR_SYNTAX), + }, + 'g' => match self.expect() { + 'e' => { + self.expect_specific_str("get", 2); + scope.routes.push(self.parse_route(WebXRouteMethod::GET)?); + }, + 'l' => { + self.expect_specific_str("global", 2); + scope.global_ts = self.parse_block('{', '}'); + }, + c => exit_error_expected_any_of_but_found("get or global".to_string(), c, self.line, self.column, ERROR_SYNTAX), + }, + 'p' => match self.expect() { + 'o' => { + self.expect_specific_str("post", 2); + scope.routes.push(self.parse_route(WebXRouteMethod::POST)?); + }, + 'u' => { + self.expect_specific_str("put", 2); + scope.routes.push(self.parse_route(WebXRouteMethod::PUT)?); + }, + 'a' => { + self.expect_specific_str("patch", 2); + scope.routes.push(self.parse_route(WebXRouteMethod::PATCH)?); + }, + c => exit_error_expected_any_of_but_found("post, put or patch".to_string(), c, self.line, self.column, ERROR_SYNTAX), + }, + 'd' => { + self.expect_specific_str("delete", 1); + scope.routes.push(self.parse_route(WebXRouteMethod::DELETE)?); + }, + 'c' => { + self.expect_specific_str("connect", 1); + scope.routes.push(self.parse_route(WebXRouteMethod::CONNECT)?); + }, + 'o' => { + self.expect_specific_str("options", 1); + scope.routes.push(self.parse_route(WebXRouteMethod::OPTIONS)?); + }, + 't' => { + self.expect_specific_str("trace", 1); + scope.routes.push(self.parse_route(WebXRouteMethod::TRACE)?); + } _ => exit_error_unexpected_char(c.unwrap(), self.line, self.column, ERROR_SYNTAX), } }