Skip to content

Commit

Permalink
Fix parse_url_path in route constructs
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamRagstad committed Sep 7, 2023
1 parent 1fc2e7f commit 4eb73db
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/engine/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod runner;
mod test;
11 changes: 11 additions & 0 deletions src/engine/test.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
243 changes: 213 additions & 30 deletions src/file/parser.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
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<char>,
}

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();
Expand All @@ -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;
Expand All @@ -57,17 +61,25 @@ 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>) -> 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.
///
/// # 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<char>, 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);
}
}

Expand All @@ -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);
}
}
}

Expand All @@ -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<char>, cs: Vec<char>) -> 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);
}
Expand All @@ -115,13 +125,32 @@ impl<'a> WebXFileParser<'a> {
None
}

fn read_until_any_of(&mut self, cs: Vec<char>) -> 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
Expand Down Expand Up @@ -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);
Expand All @@ -179,14 +208,14 @@ impl<'a> WebXFileParser<'a> {
}

fn parse_location(&mut self) -> Result<WebXScope, String> {
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 }
Expand All @@ -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)
/// - <name>(<field>: <type>, <field>: <type>, ...)
/// - user-defined model name
/// - <name>
///
/// ## Example:
/// ```ignore
/// json(text: string, n: number)
/// form(name: string, age: number)
/// User
/// ```
fn parse_body_format(&mut self) -> Option<String> {
match self.peek().unwrap() {
c if c.is_alphabetic() => self.parse_body_format(),
_ => None,
}
}

fn parse_handler_calls(&mut self) -> Vec<String> {
todo!("parse_handler_calls")
}

fn parse_route_handlers(&mut self) -> Vec<String> {
match self.next_skip_whitespace(true) {
Some('-') => {
self.expect_specific_str("->", 1);
self.parse_handler_calls()
},
_ => vec![]
}
}

fn parse_route_body(&mut self) -> Option<String> {
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 (<h1>My page</h1>)
/// post /path/to/(arg: string)/route json(text: string, n: number) -> handler(arg, text) {
/// // ...
/// }
/// ```
fn parse_route(&mut self, method: WebXRouteMethod) -> Result<WebXRoute, String> {
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.
Expand Down Expand Up @@ -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),
}
}
Expand Down

0 comments on commit 4eb73db

Please sign in to comment.