diff --git a/.gitignore b/.gitignore index 2b9af38e33382b4d389ddbcba61103463af4ef16..c1d3494b4ed75bc891ce2f3470b4b2792d1ba44e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ # src build target + +# debug tests +test.db +test.html # maid .maid/cache @@ -17,8 +21,8 @@ tests # bin bin +todo.md config.toml -test.db # fleet fleet.toml diff --git a/Cargo.lock b/Cargo.lock index a41a00e4cb7c4bec56cac163dbefc519d3c97426..2b21ae974ce2aa6cc99b949f9f9831e129dcd0be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2422,7 +2422,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "script" -version = "0.4.0" +version = "0.4.1" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index cf74ba9063de8bb743de2aea4d36e6fa57f91ee5..d0793b57a00079d63860cc6af942103a8ffa76c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "script" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "MIT" repository = "https://github.com/themackabu/script" diff --git a/Maidfile.toml b/Maidfile.toml index ec820497ab8b2cdfe23bb294669ce1353b81f1d2..2aa94fa963cdb0d0cece6071f9c59b04378100c0 100644 --- a/Maidfile.toml +++ b/Maidfile.toml @@ -1,6 +1,6 @@ [project] name = "script" -version = "0.4.0" +version = "0.4.1" [tasks] clean = { script = ["rm -rf bin", "mkdir bin"] } diff --git a/app.routes b/app.routes index 70caeb79c78a7e33f891f08d4600cff11c2e5167..2d6f4bc21e778dae657c041960e2e0dd9d89ef2b 100644 --- a/app.routes +++ b/app.routes @@ -11,6 +11,11 @@ example() { html(http::get("https://example.org").body) } +#[route("/long"), cfg(wildcard = true)] +wild_test() { + text("I am a wildcard route") +} + #[route("/db")] db() { let db = kv::load("test.db"); diff --git a/src/helpers.rs b/src/helpers.rs index 4ca456b6e28cbd8f6ee327c934fb26c633624d33..744b6c233ce4cb9d539f605552d223b676dfcc98 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -17,10 +17,12 @@ pub fn rm_first(s: &str) -> &str { pub fn convert_to_format(input: &str) -> String { let re = Regex::new(r"\.(\w+)").unwrap(); + let input = super::replace_chars(input); format!("_route_{}", re.replace_all(&input.replace("/", "_"), |captures: &Captures| format!("__d{}", rm_first(&captures[0])))) } pub fn route_to_fn(input: &str) -> String { + let input = super::replace_chars(input); let re = Regex::new(r#"\{([^{}\s]+)\}"#).unwrap(); let re_dot = Regex::new(r"\.(\w+)").unwrap(); diff --git a/src/main.rs b/src/main.rs index 3e582f0ae9dd9ba78238ea297376335c701437b1..5b4e66afa431007fa55342defec854f2cd4e2e53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod config; mod database; mod helpers; mod modules; +mod routes; mod structs; use helpers::prelude::*; @@ -13,7 +14,7 @@ use mime::Mime; use regex::{Captures, Error, Regex}; use reqwest::blocking::Client as ReqwestClient; use smartstring::alias::String as SmString; -use std::{collections::BTreeMap, fs}; +use std::{collections::HashMap, fs}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_subscriber::{filter::LevelFilter, prelude::*}; @@ -23,7 +24,7 @@ use rhai_url::UrlPackage; use macros_rs::{ exp::ternary, - fmt::{crashln, str, string}, + fmt::{crashln, string}, obj::lazy_lock, os::set_env_sync, }; @@ -37,9 +38,9 @@ use actix_web::{ lazy_lock! { static R_INDEX: Result = Regex::new(r"index\s*\{"); static R_ERR: Result = Regex::new(r"(\b\d{3})\s*\{"); - static R_FN: Result = Regex::new(r"(\w+)\((.*?)\)\s*\{"); static R_DOT: Result = Regex::new(r"\.(\w+)\((.*?)\)\s*\{"); static R_WILD: Result = Regex::new(r"\*\s*\{|wildcard\s*\{"); + static R_FN: Result = Regex::new(r"([\w#:\-@!&^~]+)\((.*?)\)\s*\{"); static R_SLASH: Result = Regex::new(r"(?m)\/(?=.*\((.*?)\)\s*\{[^{]*$)"); } @@ -76,6 +77,13 @@ pub fn proxy(url: String) -> (String, ContentType, StatusCode) { } } +fn parse_bool(s: &str) -> bool { + match s.trim().to_lowercase().as_str() { + "true" | "1" | "yes" | "on" => true, + _ => false, + } +} + fn match_route(route_template: &str, placeholders: &[&str], url: &str) -> Option> { let mut matched_placeholders = Vec::new(); @@ -99,6 +107,31 @@ fn match_route(route_template: &str, placeholders: &[&str], url: &str) -> Option Some(matched_placeholders) } +pub fn replace_chars(input: &str) -> String { + let replacements = HashMap::from([ + ('#', "__fhas"), + (':', "__fcol"), + ('-', "__fdas"), + ('@', "__fats"), + ('!', "__fexl"), + ('&', "__famp"), + ('^', "__fcar"), + ('~', "__ftil"), + ]); + + let mut result = String::with_capacity(input.len()); + + for c in input.chars() { + if let Some(replacement) = replacements.get(&c) { + result.push_str(replacement); + } else { + result.push(c); + } + } + + result +} + fn match_segment(route_segment: &str, url_segment: &str, placeholders: &[&str]) -> Option { if route_segment.starts_with('{') && route_segment.ends_with('}') { let placeholder = &route_segment[1..route_segment.len() - 1]; @@ -141,10 +174,11 @@ async fn handler(req: HttpRequest, config: Data) -> impl Responder { } if url.as_str() == "favicon.ico" { + // remove this return HttpResponse::Ok().body(""); } - let mut routes: BTreeMap> = BTreeMap::new(); + let mut routes: HashMap> = HashMap::new(); let filename = &config.workers.get(0).unwrap(); let fs_pkg = FilesystemPackage::new(); @@ -234,38 +268,85 @@ async fn handler(req: HttpRequest, config: Data) -> impl Responder { let has_wildcard = R_WILD.as_ref().unwrap().is_match(&contents).unwrap(); let has_index = R_INDEX.as_ref().unwrap().is_match(&contents).unwrap(); + fn parse_cfg(cfg_str: &str) -> HashMap { + cfg_str + .split(',') + .filter_map(|pair| { + let mut parts = pair.split('='); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + Some((key.trim().to_string(), value.trim().trim_matches('"').to_string())) + } else { + None + } + }) + .collect() + } + + fn extract_route(input: &str, route_name: Option<&str>) -> Vec { + let re = Regex::new(r"(?m)^\s*#\[route\(([^)]+)\)(?:,\s*cfg\(([^)]+)\))?\]\s*([^\(]+)\([^)]*\)\s*\{([^}]+)\}").unwrap(); + + re.captures_iter(input) + .filter_map(|captures| { + let cap = captures.unwrap(); + + let route = helpers::rm_first(cap[1].trim().trim_matches('"')).to_string(); + if route_name.map_or(true, |name| route == name) { + let cfg = cap.get(2).map(|m| parse_cfg(m.as_str())); + let fn_name = cap[3].trim().to_string(); + let fn_body = cap[4].trim().to_string(); + let fn_fmt = "".to_string(); + + Some(routes::Route { route, cfg, fn_name, fn_body, fn_fmt }) + } else { + None + } + }) + .collect() + } + + let route_data = extract_route(&contents, None); + let contents = { let pattern = r#"\{([^{}\s]+)\}"#; + let pattern_rm_config = r#",?\s*cfg\([^)]*\)"#; let pattern_combine = r#"(?m)^_route/(.*)\n(.*?)\((.*?)\)"#; let re = Regex::new(pattern).unwrap(); let re_combine = Regex::new(pattern_combine).unwrap(); + let re_rm_config = Regex::new(pattern_rm_config).unwrap(); let result = re.replace_all(&contents, |captures: ®ex::Captures| { let content = captures.get(1).map_or("", |m| m.as_str()); format!("_arg_{content}") }); - let output = result.replace("#[route(\"", "_route").replace("\")]", ""); - - re_combine.replace_all(str!(output), |captures: ®ex::Captures| { - let path = captures.get(1).map_or("", |m| m.as_str()); - let args = captures.get(3).map_or("", |m| m.as_str()); + let result = re_rm_config.replace_all(&result, ""); + let result = result.replace("#[route(\"", "_route").replace("\")]", ""); + + let new_result_fmt = re_combine + .replace_all(&result, |captures: ®ex::Captures| { + let path = captures.get(1).map_or("", |m| m.as_str()); + let args = captures.get(3).map_or("", |m| m.as_str()); + + if args != "" { + let r_path = Regex::new(r"(?m)_arg_(\w+)").unwrap(); + let key = r_path.replace_all(&path, |captures: ®ex::Captures| { + let key = captures.get(1).map_or("", |m| m.as_str()); + format!("{{{key}}}") + }); + + routes.insert(string!(key), args.split(",").map(|s| s.to_string().replace(" ", "")).collect()); + format!("fmt_{path}({args})") + } else { + routes.insert(string!(path), vec![]); + format!("{path}()") + } + }) + .into_owned(); - if args != "" { - let r_path = Regex::new(r"(?m)_arg_(\w+)").unwrap(); - let key = r_path.replace_all(&path, |captures: ®ex::Captures| { - let key = captures.get(1).map_or("", |m| m.as_str()); - format!("{{{key}}}") - }); + std::mem::drop(result); - routes.insert(string!(key), args.split(",").map(|s| s.to_string().replace(" ", "")).collect()); - format!("fmt_{path}({args})") - } else { - routes.insert(string!(path), vec![]); - format!("{path}()") - } - }) + new_result_fmt }; // cache contents until file change @@ -287,7 +368,15 @@ async fn handler(req: HttpRequest, config: Data) -> impl Responder { .unwrap() .replace_all(&result, |captures: &Captures| format!("__d{}", helpers::rm_first(&captures[0]))) .to_string(); - let result = R_FN.as_ref().unwrap().replace_all(&result, |captures: &Captures| format!("fn _route_{}", &captures[0])).to_string(); + + let result = R_FN + .as_ref() + .unwrap() + .replace_all(&result, |captures: &Captures| { + let fmt = replace_chars(&captures[0]); + format!("fn _route_{fmt}") + }) + .to_string(); ternary!(has_wildcard, R_WILD.as_ref().unwrap().replace_all(&result, "fn _wildcard() {").to_string(), result) }; @@ -364,6 +453,36 @@ async fn handler(req: HttpRequest, config: Data) -> impl Responder { } } + for data in route_data { + let name = data.route; + + let cfg = match data.cfg { + Some(cfg) => cfg, + None => HashMap::new(), + }; + + for (item, val) in cfg { + match item.as_str() { + "wildcard" => { + if url.splitn(2, '/').next().unwrap_or(&url) == name && parse_bool(&val) { + match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, helpers::convert_to_format(&name), ()) { + Ok(response) => send!(response), + Err(err) => { + let body = ServerError { + error: err.to_string().replace("\n", "
"), + context: extract_context(contents, err.to_string()), + }; + + return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).content_type(ContentType::html()).body(body.render().unwrap()); + } + } + } + } + _ => {} + } + } + } + if has_wildcard || has_error_page { let (body, content_type, status_code) = engine .call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, ternary!(has_wildcard, "_wildcard", "_route_error_404"), ()) diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000000000000000000000000000000000000..84ed0c2b0da5b8e5a24776337541c1198f808edd --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +type Config = Option>; + +#[derive(Debug, Default)] +pub struct Route { + pub cfg: Config, + pub route: String, + pub fn_name: String, + pub fn_body: String, + pub fn_fmt: String, +} + +impl Route { + pub fn new() -> Self { Default::default() } +} diff --git a/test.html b/test.html deleted file mode 100644 index 221f8a5ee98fd3936347c8104e2b176cdbce6921..0000000000000000000000000000000000000000 --- a/test.html +++ /dev/null @@ -1 +0,0 @@ -

:3