From decd2e8bda6d9c436b97be232dd2aba3d599f1d1 Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Tue, 18 Jul 2023 20:03:49 -0400 Subject: [PATCH 1/9] added some parsing logic --- src/macro_executor.rs | 248 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/src/macro_executor.rs b/src/macro_executor.rs index dff3513b..bd38bfd8 100644 --- a/src/macro_executor.rs +++ b/src/macro_executor.rs @@ -49,6 +49,9 @@ use deno_core::{anyhow, error::generic_error}; use deno_core::{resolve_import, ModuleCode}; use futures::FutureExt; +use indexmap::IndexMap; +use crate::traits::t_configurable::manifest::{ConfigurableValue, ConfigurableValueType, SettingManifest}; +use crate::util::fs; pub trait WorkerOptionGenerator: Send + Sync { fn generate(&self) -> deno_runtime::worker::WorkerOptions; @@ -564,6 +567,251 @@ impl MacroExecutor { pub async fn get_macro_status(&self, pid: MacroPID) -> Option { self.exit_status_table.get(&pid).map(|v| v.clone()) } + + pub fn get_config_manifest(path: &PathBuf) -> Result, Error> { + match extract_config_code(&fs::read_to_string(path)?) { + Ok(optional_code) => { + match optional_code { + Some(definition) => { + get_config_from_code(&definition) + }, + None => { + Ok(IndexMap::::new()) + } + } + }, + Err(e) => { + error!(e); + e + } + } + } +} + +fn extract_config_code(code: &str) -> Result, Error> { + if let Some(index) = code.find("LodestoneConfig") { + let config_code = &code[index..]; + let end_index = { + let mut open_count = 0; + let mut close_count = 0; + let mut i = 0; + for &char_item in config_code.to_string().as_bytes().iter() { + if char_item == b'{' { + open_count += 1; + } + if char_item == b'}' { + close_count += 1; + } + i += 1; + + if open_count == close_count && open_count > 0 { + break; + } + } + + if i == 0 || open_count != close_count { + return Err(ts_syntax_error("config")); + } + + i + }; + + return match config_code.find('{') { + Some(start_index) => Ok(Some(config_code[start_index..end_index].to_string())), + None => Err(ts_syntax_error("config")) + } + } + Ok(None) +} + +fn get_config_from_code(config_definition: &str) -> Result, Error> { + let str_length = config_definition.len(); + let config_params_str = &config_definition[1..str_length-1].to_string(); + let config_params_str = config_params_str.split('\n'); + + // parse config code into a collection of description and definition + let mut comment_lines: Vec = vec![]; + let mut code_lines: Vec = vec![]; + let mut comments: Vec = vec![]; + let mut codes: Vec = vec![]; + let mut comment_block_count = 0; + for (_, line) in config_params_str.enumerate() { + let line = line.replace('\r', ""); + let line = line.trim(); + + if line.is_empty() { + continue; + } + + // comments within a comment block + if comment_block_count > 0 { + // closing the comment block + if line.starts_with("*/") { + comment_block_count -= 1; + continue; + } + + // comments within a comment block + if let Some(line) = line.strip_prefix('*') { + comment_lines.push(line.trim().to_string()); + } else { + comment_lines.push(line.to_string()); + } + continue; + } + + // single line comment & opening of a comment block + if line.starts_with("//") { + comment_lines.push(line.strip_prefix("//").unwrap().trim().to_string()); + } else if line.starts_with("/**") { + comment_lines.push(line.strip_prefix("/**").unwrap().trim().to_string()); + comment_block_count += 1; + } else if line.starts_with("/*") { + comment_lines.push(line.strip_prefix("/*").unwrap().trim().to_string()); + comment_block_count += 1; + } else { + // if non of those are satisfied, it must be a line of actual code instead of a comment + let code_line = line.replace([' ', '\t'], "").trim().to_string(); + code_lines.push(code_line.clone()); + if code_line.ends_with(';') { + comments.push(comment_lines.join(" ")); + comment_lines.clear(); + codes.push(code_lines.join("").strip_suffix(';').unwrap().to_string()); + code_lines.clear(); + } + } + } + + let mut configs: IndexMap = IndexMap::new(); + + Ok(configs) +} + +fn parse_config_single( + single_config_definition: &str, + config_description: &str, +) -> Result<(String, SettingManifest), Error> { + let entry = single_config_definition.trim().to_string(); + + let (name_end_index, type_start_index) = match entry.find('?') { + Some(index) => (index, index+2), + None => { + match entry.find(':') { + Some(index) => (index, index+1), + None => { + return Err(ts_syntax_error("config")); + } + } + } + }; + let var_name = &entry[..name_end_index]; + let is_optional = name_end_index+2 == type_start_index; + + let default_value_index = match entry.find('=') { + Some(index) => index, + None => entry.len(), + }; + if type_start_index >= default_value_index { + return Err(ts_syntax_error("config")); + } + + let var_type = &entry[type_start_index..default_value_index]; + let config_type = get_config_value_type(var_type)?; + let has_default = default_value_index != entry.len(); + + if !is_optional && !has_default { + return Err(Error::new( + ErrorKind::InvalidData, + format!("{var_name} is not optional and thus must have a default value") + )); + } + + let default_val = if has_default { + let val_str = entry[default_value_index + 1..].to_string(); + let val_str_len = val_str.len(); + Some(match config_type { + ConfigurableValueType::String => ConfigurableValue::String(val_str[1..val_str_len-1].to_string()), + ConfigurableValueType::Boolean => { + let value = match val_str.parse::() { + Ok(val) => val, + Err(_) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Cannot parse \"{val_str}\" to a bool") + )); + } + }; + ConfigurableValue::Boolean(value) + }, + ConfigurableValueType::Float => { + let value = match val_str.parse::() { + Ok(val) => val, + Err(_) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Cannot parse \"{val_str}\" to a number") + )); + } + }; + ConfigurableValue::Float(value) + }, + ConfigurableValueType::Enum{ .. } => ConfigurableValue::Enum(val_str[1..val_str_len-1].to_string()), + _ => None, + }) + } else { + None + }; + + Ok((var_name.to_string(), SettingManifest::new( + var_name, + config_description, + default_val.clone(), + config_type, + default_val, + is_optional, + ))) +} + +fn get_config_value_type(type_str: &str) -> Result { + let result = match type_str { + "string" => ConfigurableValueType::String, + "boolean" => ConfigurableValueType::Boolean, + "number" => ConfigurableValueType::Float, + _ => { + // try to parse it into an enum + let enum_options = type_str.split('|'); + let mut options: Vec = Vec::new(); + + for (_, option) in enum_options.enumerate() { + let error = Error::new( + ErrorKind::InvalidData, + format!("cannot parse type \"{}\"", type_str) + ); + + // verify the enum options are strings + let first_quote_index = { + if let Some(i) = option.find('\'') { + i + } else if let Some(i) = option.find('"') { + i + } else { + return Err(error); + } + }; + + if first_quote_index == 0 { + let str_len = option.len(); + options.push(option[1..str_len-1].to_string()); + } + else { + return Err(error); + } + } + + ConfigurableValueType::Enum { options } + } + }; + Ok(result) } #[cfg(test)] From ff2a2dc4f355ade15d1aa35367b2644d72c6cc66 Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Wed, 19 Jul 2023 21:28:58 -0400 Subject: [PATCH 2/9] added parser - fixed rebase --- .../.vscode => .vscode}/extensions.json | 4 +- core/src/error.rs | 9 +++ core/src/macro_executor.rs | 79 ++++++++++--------- .../DashboardLayout/NotificationPopover.tsx | 3 +- 4 files changed, 56 insertions(+), 39 deletions(-) rename {dashboard/.vscode => .vscode}/extensions.json (51%) diff --git a/dashboard/.vscode/extensions.json b/.vscode/extensions.json similarity index 51% rename from dashboard/.vscode/extensions.json rename to .vscode/extensions.json index 50735635..b4f57568 100644 --- a/dashboard/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,8 @@ "recommendations": [ "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "rust-lang.rust-analyzer", + "swellaby.rust-pack" ] } diff --git a/core/src/error.rs b/core/src/error.rs index 1a683616..f940d9e7 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -37,6 +37,15 @@ impl Error { } } +impl Error { + pub fn ts_syntax_error(context: &str) -> Error { + Error { + kind: ErrorKind::Internal, + source: Report::msg(format!("Syntax error parsing ts ({context})")), + } + } +} + impl Display for ErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index bd38bfd8..47f9338f 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -7,6 +7,7 @@ use std::{ Arc, }, time::Duration, + iter::zip, }; use color_eyre::eyre::Context; @@ -568,8 +569,8 @@ impl MacroExecutor { self.exit_status_table.get(&pid).map(|v| v.clone()) } - pub fn get_config_manifest(path: &PathBuf) -> Result, Error> { - match extract_config_code(&fs::read_to_string(path)?) { + pub async fn get_config_manifest(path: &PathBuf) -> Result, Error> { + match extract_config_code(&fs::read_to_string(path).await?) { Ok(optional_code) => { match optional_code { Some(definition) => { @@ -581,8 +582,7 @@ impl MacroExecutor { } }, Err(e) => { - error!(e); - e + Err(e) } } } @@ -610,7 +610,7 @@ fn extract_config_code(code: &str) -> Result, Error> { } if i == 0 || open_count != close_count { - return Err(ts_syntax_error("config")); + return Err(Error::ts_syntax_error("config")); } i @@ -618,7 +618,7 @@ fn extract_config_code(code: &str) -> Result, Error> { return match config_code.find('{') { Some(start_index) => Ok(Some(config_code[start_index..end_index].to_string())), - None => Err(ts_syntax_error("config")) + None => Err(Error::ts_syntax_error("config")) } } Ok(None) @@ -683,6 +683,10 @@ fn get_config_from_code(config_definition: &str) -> Result = IndexMap::new(); + for (definition, desc) in zip(codes, comments) { + let (name, config) = parse_config_single(&definition, &desc)?; + configs.insert(name, config); + } Ok(configs) } @@ -699,7 +703,7 @@ fn parse_config_single( match entry.find(':') { Some(index) => (index, index+1), None => { - return Err(ts_syntax_error("config")); + return Err(Error::ts_syntax_error("config")); } } } @@ -712,7 +716,7 @@ fn parse_config_single( None => entry.len(), }; if type_start_index >= default_value_index { - return Err(ts_syntax_error("config")); + return Err(Error::ts_syntax_error("config")); } let var_type = &entry[type_start_index..default_value_index]; @@ -720,74 +724,71 @@ fn parse_config_single( let has_default = default_value_index != entry.len(); if !is_optional && !has_default { - return Err(Error::new( - ErrorKind::InvalidData, - format!("{var_name} is not optional and thus must have a default value") - )); + return Err(Error { + kind: ErrorKind::NotFound, + source: eyre!("{var_name} is not optional and thus must have a default value"), + }); } let default_val = if has_default { let val_str = entry[default_value_index + 1..].to_string(); let val_str_len = val_str.len(); Some(match config_type { - ConfigurableValueType::String => ConfigurableValue::String(val_str[1..val_str_len-1].to_string()), + ConfigurableValueType::String{ .. } => ConfigurableValue::String(val_str[1..val_str_len-1].to_string()), ConfigurableValueType::Boolean => { let value = match val_str.parse::() { Ok(val) => val, Err(_) => { - return Err(Error::new( - ErrorKind::InvalidData, - format!("Cannot parse \"{val_str}\" to a bool") - )); + return Err(Error { + kind: ErrorKind::Internal, + source: eyre!("Cannot parse \"{val_str}\" to a bool") + }); } }; ConfigurableValue::Boolean(value) }, - ConfigurableValueType::Float => { + ConfigurableValueType::Float{ .. } => { let value = match val_str.parse::() { Ok(val) => val, Err(_) => { - return Err(Error::new( - ErrorKind::InvalidData, - format!("Cannot parse \"{val_str}\" to a number") - )); + return Err(Error { + kind: ErrorKind::Internal, + source: eyre!("Cannot parse \"{val_str}\" to a number") + }); } }; ConfigurableValue::Float(value) }, ConfigurableValueType::Enum{ .. } => ConfigurableValue::Enum(val_str[1..val_str_len-1].to_string()), - _ => None, + _ => panic!("TS config parsing error: invlid type not caught by the type parser"), }) } else { None }; - Ok((var_name.to_string(), SettingManifest::new( - var_name, - config_description, + Ok((var_name.to_string(), SettingManifest::new_value_with_type( + var_name.to_string(), + var_name.to_string(), + config_description.to_string(), default_val.clone(), config_type, default_val, - is_optional, + false, + true, ))) } fn get_config_value_type(type_str: &str) -> Result { let result = match type_str { - "string" => ConfigurableValueType::String, + "string" => ConfigurableValueType::String{ regex: None }, "boolean" => ConfigurableValueType::Boolean, - "number" => ConfigurableValueType::Float, + "number" => ConfigurableValueType::Float{ max: None, min: None}, _ => { // try to parse it into an enum let enum_options = type_str.split('|'); let mut options: Vec = Vec::new(); for (_, option) in enum_options.enumerate() { - let error = Error::new( - ErrorKind::InvalidData, - format!("cannot parse type \"{}\"", type_str) - ); - // verify the enum options are strings let first_quote_index = { if let Some(i) = option.find('\'') { @@ -795,7 +796,10 @@ fn get_config_value_type(type_str: &str) -> Result } else if let Some(i) = option.find('"') { i } else { - return Err(error); + return Err(Error { + kind: ErrorKind::Internal, + source: eyre!("cannot parse type \"{}\"", type_str) + }); } }; @@ -804,7 +808,10 @@ fn get_config_value_type(type_str: &str) -> Result options.push(option[1..str_len-1].to_string()); } else { - return Err(error); + return Err(Error { + kind: ErrorKind::Internal, + source: eyre!("cannot parse type \"{}\"", type_str) + }); } } diff --git a/dashboard/src/components/DashboardLayout/NotificationPopover.tsx b/dashboard/src/components/DashboardLayout/NotificationPopover.tsx index 93a569c4..9d5451cc 100644 --- a/dashboard/src/components/DashboardLayout/NotificationPopover.tsx +++ b/dashboard/src/components/DashboardLayout/NotificationPopover.tsx @@ -44,7 +44,6 @@ export const NotificationPopover = () => { }; useEffect(() => { - console.log(notifications, ongoingNotifications) setPreviousNotifications(notifications); setNewNotifications(shouldShowPopup()); }, [notifications, ongoingNotifications]); @@ -66,4 +65,4 @@ export const NotificationPopover = () => { ); -} \ No newline at end of file +} From 186b879fe93921b4d0edc25fbd0d8241d5d5162e Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Sun, 23 Jul 2023 23:42:06 -0400 Subject: [PATCH 3/9] collected string split results - fixed rebase --- Cargo.lock | 15 +++++++++++++++ .../generic/js/main/libs/procedure_bridge.ts | 2 +- core/src/macro_executor.rs | 8 ++++---- dashboard/src-tauri/Cargo.lock | 15 +++++++++++++++ dashboard/src-tauri/Cargo.toml | 1 + dashboard/src-tauri/src/main.rs | 9 +++++++++ dashboard/src-tauri/tauri.conf.json | 2 +- .../src/components/Atoms/Config/InputBox.tsx | 10 ++++++---- 8 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7433d100..4f79780b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4165,6 +4165,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-localhost", + "tauri-plugin-single-instance", "tokio", ] @@ -7679,6 +7680,20 @@ dependencies = [ "tiny_http", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#51f20b438e42050cdbfd6c6dc72dbc985a31bbc1" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.48.0", + "zbus", +] + [[package]] name = "tauri-runtime" version = "0.14.0" diff --git a/core/src/implementations/generic/js/main/libs/procedure_bridge.ts b/core/src/implementations/generic/js/main/libs/procedure_bridge.ts index de918919..cc466541 100644 --- a/core/src/implementations/generic/js/main/libs/procedure_bridge.ts +++ b/core/src/implementations/generic/js/main/libs/procedure_bridge.ts @@ -246,7 +246,7 @@ export async function procedure_bridge(instance: AtomInstance,) { } } if (inner.type === "SetupInstance") { - await instance.setup(inner.setup_value, inner.dot_lodestone_config, ProgressionHandler.__private__create(inner.progression_event_id), inner.path) + await instance.setup(inner.setup_value, inner.dot_lodestone_config, ProgressionHandler.__private__create(inner.progression_event_id, 100), inner.path) } else if (inner.type == "RestoreInstance") { await instance.restore(inner.dot_lodestone_config, inner.path); diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index 47f9338f..327ca897 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -627,7 +627,7 @@ fn extract_config_code(code: &str) -> Result, Error> { fn get_config_from_code(config_definition: &str) -> Result, Error> { let str_length = config_definition.len(); let config_params_str = &config_definition[1..str_length-1].to_string(); - let config_params_str = config_params_str.split('\n'); + let config_params_str: Vec<_> = config_params_str.split('\n').collect(); // parse config code into a collection of description and definition let mut comment_lines: Vec = vec![]; @@ -635,7 +635,7 @@ fn get_config_from_code(config_definition: &str) -> Result = vec![]; let mut codes: Vec = vec![]; let mut comment_block_count = 0; - for (_, line) in config_params_str.enumerate() { + for line in config_params_str { let line = line.replace('\r', ""); let line = line.trim(); @@ -785,10 +785,10 @@ fn get_config_value_type(type_str: &str) -> Result "number" => ConfigurableValueType::Float{ max: None, min: None}, _ => { // try to parse it into an enum - let enum_options = type_str.split('|'); + let enum_options: Vec<_> = type_str.split('|').collect(); let mut options: Vec = Vec::new(); - for (_, option) in enum_options.enumerate() { + for option in enum_options { // verify the enum options are strings let first_quote_index = { if let Some(i) = option.find('\'') { diff --git a/dashboard/src-tauri/Cargo.lock b/dashboard/src-tauri/Cargo.lock index 4dda28e3..b2f6f97a 100644 --- a/dashboard/src-tauri/Cargo.lock +++ b/dashboard/src-tauri/Cargo.lock @@ -4126,6 +4126,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-localhost", + "tauri-plugin-single-instance", "tokio", ] @@ -7666,6 +7667,20 @@ dependencies = [ "tiny_http", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#180ec441aa64654e23369be3ed86c2a1fa99a803" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.48.0", + "zbus", +] + [[package]] name = "tauri-runtime" version = "0.14.0" diff --git a/dashboard/src-tauri/Cargo.toml b/dashboard/src-tauri/Cargo.toml index 2347a28e..ba9e29b1 100644 --- a/dashboard/src-tauri/Cargo.toml +++ b/dashboard/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-plugin-localhost = "0.1.0" portpicker = "0.1" lodestone_core = { path = "../../core" } tokio = { version = "1.21.1", features = ["macros", "rt"] } +tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } [features] # by default Tauri runs in production mode diff --git a/dashboard/src-tauri/src/main.rs b/dashboard/src-tauri/src/main.rs index 2538e157..c044edd7 100644 --- a/dashboard/src-tauri/src/main.rs +++ b/dashboard/src-tauri/src/main.rs @@ -14,6 +14,12 @@ use tauri::Manager; use tauri::{utils::config::AppUrl, WindowUrl}; use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu}; +#[derive(Clone, serde::Serialize)] +struct Payload { + args: Vec, + cwd: String, +} + #[tauri::command] async fn is_setup(state: tauri::State<'_, AppState>) -> Result { Ok(is_owner_account_present(state.inner()).await) @@ -76,6 +82,9 @@ async fn main() { let tray_menu = SystemTrayMenu::new().add_item(quit); builder + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + app.emit_all("single-instance", Payload { args: argv, cwd }).unwrap(); + })) .manage(app_state) .invoke_handler(tauri::generate_handler![ is_setup, diff --git a/dashboard/src-tauri/tauri.conf.json b/dashboard/src-tauri/tauri.conf.json index 9de72dc6..df40400e 100644 --- a/dashboard/src-tauri/tauri.conf.json +++ b/dashboard/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "Lodestone", - "version": "0.5.0-2" + "version": "0.5.0-3" }, "tauri": { "allowlist": { diff --git a/dashboard/src/components/Atoms/Config/InputBox.tsx b/dashboard/src/components/Atoms/Config/InputBox.tsx index a25bd46e..1e2e5b2a 100644 --- a/dashboard/src/components/Atoms/Config/InputBox.tsx +++ b/dashboard/src/components/Atoms/Config/InputBox.tsx @@ -170,7 +170,8 @@ export default function InputBox({ let icons = []; - if (typeModified !== 'password' && touched) { + // enable editing regardless of input type + if (touched) { icons.push( @@ -255,7 +257,7 @@ export default function InputBox({ { setValue(value.trim()); }} - disabled={typeModified === 'password' ? true : disabled} + disabled={disabled} type={typeModified} autoComplete={DISABLE_AUTOFILL} /> From f2ab57e291ebb225f643781be61d59d407c86711 Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Tue, 25 Jul 2023 21:37:26 -0400 Subject: [PATCH 4/9] added declaration variable name parsing --- core/src/macro_executor.rs | 127 +++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index 327ca897..bbb7057e 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -573,8 +573,8 @@ impl MacroExecutor { match extract_config_code(&fs::read_to_string(path).await?) { Ok(optional_code) => { match optional_code { - Some(definition) => { - get_config_from_code(&definition) + Some((var_name, definition)) => { + get_config_from_code(&var_name, &definition) }, None => { Ok(IndexMap::::new()) @@ -588,43 +588,98 @@ impl MacroExecutor { } } -fn extract_config_code(code: &str) -> Result, Error> { - if let Some(index) = code.find("LodestoneConfig") { - let config_code = &code[index..]; - let end_index = { - let mut open_count = 0; - let mut close_count = 0; - let mut i = 0; - for &char_item in config_code.to_string().as_bytes().iter() { - if char_item == b'{' { - open_count += 1; - } - if char_item == b'}' { - close_count += 1; - } - i += 1; +fn extract_config_code(code: &str) -> Result, Error> { + let config_indices: Vec<_> = code.match_indices("LodestoneConfig").collect(); + if config_indices.len() < 2 { + return Ok(None); + } - if open_count == close_count && open_count > 0 { - break; - } + // first occurrence of LodeStoneConfig must be the class declaration + let config_code = &code[(config_indices[0].0)..]; + let end_index = { + let mut open_count = 0; + let mut close_count = 0; + let mut i = 0; + for &char_item in config_code.to_string().as_bytes().iter() { + if char_item == b'{' { + open_count += 1; + } + if char_item == b'}' { + close_count += 1; } + i += 1; - if i == 0 || open_count != close_count { - return Err(Error::ts_syntax_error("config")); + if open_count == close_count && open_count > 0 { + break; } + } - i - }; + if i == 0 || open_count != close_count { + return Err(Error::ts_syntax_error("config")); + } + + i + }; + + // second occurrence of LodeStoneConfig must be the config variable declaration + let config_var_code = { + let second_occur_index = config_indices[1].0 - config_indices[0].0 + "LodestoneConfig".len(); + &config_code[end_index..second_occur_index] + }; - return match config_code.find('{') { - Some(start_index) => Ok(Some(config_code[start_index..end_index].to_string())), - None => Err(Error::ts_syntax_error("config")) + // parse whether the keyword 'var', 'let', or 'const' is used + let decl_keyword = { + let mut config_var_tokens: Vec<_> = config_var_code.split(' ').collect(); + config_var_tokens.reverse(); + + let mut keyword: &str = ""; + let keywords = vec!["let", "const", "var"]; + for token in config_var_tokens { + if keywords.contains(&token) { + keyword = token; + break; + } + } + if keyword.is_empty() { + return Err(Error::ts_syntax_error( + "Class definition detected but cannot find config declaration" + )); } + keyword + }; + + let config_var_code = config_var_code.replace(' ', ""); + let config_var_name = { + let decl_keyword_index = match config_var_code.match_indices(decl_keyword).collect::>().last() { + Some(val) => val.0, + None => { + return Err(Error::ts_syntax_error( + "Class definition detected but cannot find config declaration" + )); + } + }; + + let decl_var_statement = &config_var_code[decl_keyword_index..]; + let var_name_end_index = match decl_var_statement.find(':') { + Some(index) => index, + None => { + return Err(Error::ts_syntax_error( + "Class definition detected but cannot find config declaration" + )); + } + }; + &decl_var_statement[decl_keyword.len()..var_name_end_index] + }; + + match config_code.find('{') { + Some(start_index) => Ok(Some( + (config_var_name.to_string(), config_code[start_index..end_index].to_string()) + )), + None => Err(Error::ts_syntax_error("config")) } - Ok(None) } -fn get_config_from_code(config_definition: &str) -> Result, Error> { +fn get_config_from_code(config_var_name: &str, config_definition: &str) -> Result, Error> { let str_length = config_definition.len(); let config_params_str = &config_definition[1..str_length-1].to_string(); let config_params_str: Vec<_> = config_params_str.split('\n').collect(); @@ -684,7 +739,11 @@ fn get_config_from_code(config_definition: &str) -> Result = IndexMap::new(); for (definition, desc) in zip(codes, comments) { - let (name, config) = parse_config_single(&definition, &desc)?; + let (name, config) = parse_config_single( + &definition, + &desc, + config_var_name, + )?; configs.insert(name, config); } @@ -694,6 +753,7 @@ fn get_config_from_code(config_definition: &str) -> Result Result<(String, SettingManifest), Error> { let entry = single_config_definition.trim().to_string(); @@ -760,14 +820,17 @@ fn parse_config_single( ConfigurableValue::Float(value) }, ConfigurableValueType::Enum{ .. } => ConfigurableValue::Enum(val_str[1..val_str_len-1].to_string()), - _ => panic!("TS config parsing error: invlid type not caught by the type parser"), + _ => panic!("TS config parsing error: invalid type not caught by the type parser"), }) } else { None }; + let mut settings_id = setting_id_prefix.to_string(); + settings_id.push('|'); + settings_id.push_str(var_name); Ok((var_name.to_string(), SettingManifest::new_value_with_type( - var_name.to_string(), + settings_id, var_name.to_string(), config_description.to_string(), default_val.clone(), From a526327fa9e7b2be4c617ac807d344b0de53b825 Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Wed, 26 Jul 2023 21:45:27 -0400 Subject: [PATCH 5/9] formatted --- core/src/macro_executor.rs | 137 +++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 65 deletions(-) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index bbb7057e..4ffd4274 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -1,5 +1,6 @@ use std::{ fmt::{Debug, Display}, + iter::zip, path::PathBuf, rc::Rc, sync::{ @@ -7,7 +8,6 @@ use std::{ Arc, }, time::Duration, - iter::zip, }; use color_eyre::eyre::Context; @@ -49,10 +49,12 @@ use deno_core::ResolutionKind; use deno_core::{anyhow, error::generic_error}; use deno_core::{resolve_import, ModuleCode}; +use crate::traits::t_configurable::manifest::{ + ConfigurableValue, ConfigurableValueType, SettingManifest, +}; +use crate::util::fs; use futures::FutureExt; use indexmap::IndexMap; -use crate::traits::t_configurable::manifest::{ConfigurableValue, ConfigurableValueType, SettingManifest}; -use crate::util::fs; pub trait WorkerOptionGenerator: Send + Sync { fn generate(&self) -> deno_runtime::worker::WorkerOptions; @@ -569,21 +571,15 @@ impl MacroExecutor { self.exit_status_table.get(&pid).map(|v| v.clone()) } - pub async fn get_config_manifest(path: &PathBuf) -> Result, Error> { + pub async fn get_config_manifest( + path: &PathBuf, + ) -> Result, Error> { match extract_config_code(&fs::read_to_string(path).await?) { - Ok(optional_code) => { - match optional_code { - Some((var_name, definition)) => { - get_config_from_code(&var_name, &definition) - }, - None => { - Ok(IndexMap::::new()) - } - } + Ok(optional_code) => match optional_code { + Some((var_name, definition)) => get_config_from_code(&var_name, &definition), + None => Ok(IndexMap::::new()), }, - Err(e) => { - Err(e) - } + Err(e) => Err(e), } } } @@ -623,7 +619,8 @@ fn extract_config_code(code: &str) -> Result, Error> { // second occurrence of LodeStoneConfig must be the config variable declaration let config_var_code = { - let second_occur_index = config_indices[1].0 - config_indices[0].0 + "LodestoneConfig".len(); + let second_occur_index = + config_indices[1].0 - config_indices[0].0 + "LodestoneConfig".len(); &config_code[end_index..second_occur_index] }; @@ -642,7 +639,7 @@ fn extract_config_code(code: &str) -> Result, Error> { } if keyword.is_empty() { return Err(Error::ts_syntax_error( - "Class definition detected but cannot find config declaration" + "Class definition detected but cannot find config declaration", )); } keyword @@ -650,11 +647,15 @@ fn extract_config_code(code: &str) -> Result, Error> { let config_var_code = config_var_code.replace(' ', ""); let config_var_name = { - let decl_keyword_index = match config_var_code.match_indices(decl_keyword).collect::>().last() { + let decl_keyword_index = match config_var_code + .match_indices(decl_keyword) + .collect::>() + .last() + { Some(val) => val.0, None => { return Err(Error::ts_syntax_error( - "Class definition detected but cannot find config declaration" + "Class definition detected but cannot find config declaration", )); } }; @@ -664,7 +665,7 @@ fn extract_config_code(code: &str) -> Result, Error> { Some(index) => index, None => { return Err(Error::ts_syntax_error( - "Class definition detected but cannot find config declaration" + "Class definition detected but cannot find config declaration", )); } }; @@ -672,16 +673,20 @@ fn extract_config_code(code: &str) -> Result, Error> { }; match config_code.find('{') { - Some(start_index) => Ok(Some( - (config_var_name.to_string(), config_code[start_index..end_index].to_string()) - )), - None => Err(Error::ts_syntax_error("config")) + Some(start_index) => Ok(Some(( + config_var_name.to_string(), + config_code[start_index..end_index].to_string(), + ))), + None => Err(Error::ts_syntax_error("config")), } } -fn get_config_from_code(config_var_name: &str, config_definition: &str) -> Result, Error> { +fn get_config_from_code( + config_var_name: &str, + config_definition: &str, +) -> Result, Error> { let str_length = config_definition.len(); - let config_params_str = &config_definition[1..str_length-1].to_string(); + let config_params_str = &config_definition[1..str_length - 1].to_string(); let config_params_str: Vec<_> = config_params_str.split('\n').collect(); // parse config code into a collection of description and definition @@ -739,11 +744,7 @@ fn get_config_from_code(config_var_name: &str, config_definition: &str) -> Resul let mut configs: IndexMap = IndexMap::new(); for (definition, desc) in zip(codes, comments) { - let (name, config) = parse_config_single( - &definition, - &desc, - config_var_name, - )?; + let (name, config) = parse_config_single(&definition, &desc, config_var_name)?; configs.insert(name, config); } @@ -758,18 +759,16 @@ fn parse_config_single( let entry = single_config_definition.trim().to_string(); let (name_end_index, type_start_index) = match entry.find('?') { - Some(index) => (index, index+2), - None => { - match entry.find(':') { - Some(index) => (index, index+1), - None => { - return Err(Error::ts_syntax_error("config")); - } + Some(index) => (index, index + 2), + None => match entry.find(':') { + Some(index) => (index, index + 1), + None => { + return Err(Error::ts_syntax_error("config")); } - } + }, }; let var_name = &entry[..name_end_index]; - let is_optional = name_end_index+2 == type_start_index; + let is_optional = name_end_index + 2 == type_start_index; let default_value_index = match entry.find('=') { Some(index) => index, @@ -794,32 +793,36 @@ fn parse_config_single( let val_str = entry[default_value_index + 1..].to_string(); let val_str_len = val_str.len(); Some(match config_type { - ConfigurableValueType::String{ .. } => ConfigurableValue::String(val_str[1..val_str_len-1].to_string()), + ConfigurableValueType::String { .. } => { + ConfigurableValue::String(val_str[1..val_str_len - 1].to_string()) + } ConfigurableValueType::Boolean => { let value = match val_str.parse::() { Ok(val) => val, Err(_) => { return Err(Error { kind: ErrorKind::Internal, - source: eyre!("Cannot parse \"{val_str}\" to a bool") + source: eyre!("Cannot parse \"{val_str}\" to a bool"), }); } }; ConfigurableValue::Boolean(value) - }, - ConfigurableValueType::Float{ .. } => { + } + ConfigurableValueType::Float { .. } => { let value = match val_str.parse::() { Ok(val) => val, Err(_) => { return Err(Error { kind: ErrorKind::Internal, - source: eyre!("Cannot parse \"{val_str}\" to a number") + source: eyre!("Cannot parse \"{val_str}\" to a number"), }); } }; ConfigurableValue::Float(value) - }, - ConfigurableValueType::Enum{ .. } => ConfigurableValue::Enum(val_str[1..val_str_len-1].to_string()), + } + ConfigurableValueType::Enum { .. } => { + ConfigurableValue::Enum(val_str[1..val_str_len - 1].to_string()) + } _ => panic!("TS config parsing error: invalid type not caught by the type parser"), }) } else { @@ -829,23 +832,29 @@ fn parse_config_single( let mut settings_id = setting_id_prefix.to_string(); settings_id.push('|'); settings_id.push_str(var_name); - Ok((var_name.to_string(), SettingManifest::new_value_with_type( - settings_id, + Ok(( var_name.to_string(), - config_description.to_string(), - default_val.clone(), - config_type, - default_val, - false, - true, - ))) + SettingManifest::new_value_with_type( + settings_id, + var_name.to_string(), + config_description.to_string(), + default_val.clone(), + config_type, + default_val, + false, + true, + ), + )) } fn get_config_value_type(type_str: &str) -> Result { let result = match type_str { - "string" => ConfigurableValueType::String{ regex: None }, + "string" => ConfigurableValueType::String { regex: None }, "boolean" => ConfigurableValueType::Boolean, - "number" => ConfigurableValueType::Float{ max: None, min: None}, + "number" => ConfigurableValueType::Float { + max: None, + min: None, + }, _ => { // try to parse it into an enum let enum_options: Vec<_> = type_str.split('|').collect(); @@ -861,19 +870,18 @@ fn get_config_value_type(type_str: &str) -> Result } else { return Err(Error { kind: ErrorKind::Internal, - source: eyre!("cannot parse type \"{}\"", type_str) + source: eyre!("cannot parse type \"{}\"", type_str), }); } }; if first_quote_index == 0 { let str_len = option.len(); - options.push(option[1..str_len-1].to_string()); - } - else { + options.push(option[1..str_len - 1].to_string()); + } else { return Err(Error { kind: ErrorKind::Internal, - source: eyre!("cannot parse type \"{}\"", type_str) + source: eyre!("cannot parse type \"{}\"", type_str), }); } } @@ -968,7 +976,6 @@ mod tests { async fn test_http_url() { tracing_subscriber::fmt::try_init(); - let (event_broadcaster, _rx) = EventBroadcaster::new(10); // construct a macro executor let executor = From 7cbe176c862bca3b4731050b627f0317249bac6d Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Sun, 30 Jul 2023 18:18:14 -0400 Subject: [PATCH 6/9] used iter for declaration name parsing --- core/src/macro_executor.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index 4ffd4274..c19ff475 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -629,20 +629,16 @@ fn extract_config_code(code: &str) -> Result, Error> { let mut config_var_tokens: Vec<_> = config_var_code.split(' ').collect(); config_var_tokens.reverse(); - let mut keyword: &str = ""; - let keywords = vec!["let", "const", "var"]; - for token in config_var_tokens { - if keywords.contains(&token) { - keyword = token; - break; - } - } - if keyword.is_empty() { - return Err(Error::ts_syntax_error( + let keywords = ["let", "const", "var"]; + let keyword_found = config_var_tokens.iter().find( + |&kw| keywords.contains(kw) + ); + match keyword_found { + Some(&kw) => kw, + None => return Err(Error::ts_syntax_error( "Class definition detected but cannot find config declaration", - )); + )) } - keyword }; let config_var_code = config_var_code.replace(' ', ""); From 38eb6ee9b4127e342604308942c901357b9fb3af Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Mon, 31 Jul 2023 21:59:44 -0400 Subject: [PATCH 7/9] added more comments --- core/src/macro_executor.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index c19ff475..93051831 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -592,10 +592,12 @@ fn extract_config_code(code: &str) -> Result, Error> { // first occurrence of LodeStoneConfig must be the class declaration let config_code = &code[(config_indices[0].0)..]; + // end_index is for extracting the class definition let end_index = { let mut open_count = 0; let mut close_count = 0; let mut i = 0; + // match for brackets to determine where the class definition ends for &char_item in config_code.to_string().as_bytes().iter() { if char_item == b'{' { open_count += 1; @@ -618,6 +620,7 @@ fn extract_config_code(code: &str) -> Result, Error> { }; // second occurrence of LodeStoneConfig must be the config variable declaration + // idea: slice from the end of class definition to 'let/const/var (name): LodestoneConfig' let config_var_code = { let second_occur_index = config_indices[1].0 - config_indices[0].0 + "LodestoneConfig".len(); @@ -643,6 +646,8 @@ fn extract_config_code(code: &str) -> Result, Error> { let config_var_code = config_var_code.replace(' ', ""); let config_var_name = { + // now check for the last occurrence of the keyword to find the starting index of + // 'let/const/var (name): LodestoneConfig' let decl_keyword_index = match config_var_code .match_indices(decl_keyword) .collect::>() @@ -656,7 +661,9 @@ fn extract_config_code(code: &str) -> Result, Error> { } }; + // slice from the keyword to the end to isolate 'let/const/var (name): LodestoneConfig' let decl_var_statement = &config_var_code[decl_keyword_index..]; + // since spaces are removed, ':' is the separator between name and 'LodestoneConfig' let var_name_end_index = match decl_var_statement.find(':') { Some(index) => index, None => { @@ -665,12 +672,16 @@ fn extract_config_code(code: &str) -> Result, Error> { )); } }; + + // the name is in between the keyword and ':' &decl_var_statement[decl_keyword.len()..var_name_end_index] }; + // last sanity check: class definition must start with a '{' match config_code.find('{') { Some(start_index) => Ok(Some(( config_var_name.to_string(), + // we no longer need the declaration, so slice after the '{' config_code[start_index..end_index].to_string(), ))), None => Err(Error::ts_syntax_error("config")), @@ -754,6 +765,7 @@ fn parse_config_single( ) -> Result<(String, SettingManifest), Error> { let entry = single_config_definition.trim().to_string(); + // compute indices to isolate class field names and types let (name_end_index, type_start_index) = match entry.find('?') { Some(index) => (index, index + 2), None => match entry.find(':') { @@ -764,6 +776,7 @@ fn parse_config_single( }, }; let var_name = &entry[..name_end_index]; + // if the field name is 2 char from the type, it must be optional ('?:') let is_optional = name_end_index + 2 == type_start_index; let default_value_index = match entry.find('=') { @@ -786,6 +799,7 @@ fn parse_config_single( } let default_val = if has_default { + // default value as string let val_str = entry[default_value_index + 1..].to_string(); let val_str_len = val_str.len(); Some(match config_type { From 0a48dd5b99bcf53c5d0def6a4dd80fed1864faaf Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Mon, 31 Jul 2023 23:24:47 -0400 Subject: [PATCH 8/9] added unit test and more explicit error message --- core/src/macro_executor.rs | 132 ++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index 93051831..cf0e4ec6 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -584,11 +584,22 @@ impl MacroExecutor { } } +/// +/// extract the class definition and the name of the declared config instance +/// from the typescript code +/// +/// *returns (instance_name, class_definition)* +/// fn extract_config_code(code: &str) -> Result, Error> { let config_indices: Vec<_> = code.match_indices("LodestoneConfig").collect(); - if config_indices.len() < 2 { + if config_indices.is_empty() { return Ok(None); } + if config_indices.len() < 2 { + return Err(Error::ts_syntax_error( + "Class definition or config declaration is missing", + )); + } // first occurrence of LodeStoneConfig must be the class declaration let config_code = &code[(config_indices[0].0)..]; @@ -692,8 +703,10 @@ fn get_config_from_code( config_var_name: &str, config_definition: &str, ) -> Result, Error> { + // remove the open and close brackets let str_length = config_definition.len(); let config_params_str = &config_definition[1..str_length - 1].to_string(); + let config_params_str: Vec<_> = config_params_str.split('\n').collect(); // parse config code into a collection of description and definition @@ -913,7 +926,8 @@ mod tests { use crate::event_broadcaster::EventBroadcaster; use crate::events::CausedBy; - use crate::macro_executor::SpawnResult; + use crate::macro_executor::{extract_config_code, get_config_from_code, parse_config_single, SpawnResult}; + use crate::traits::t_configurable::manifest::ConfigurableValue; struct BasicMainWorkerGenerator; @@ -1022,6 +1036,120 @@ mod tests { .unwrap(); exit_future.await.unwrap(); } + + #[test] + fn test_macro_config_extraction() { + // should return None if no there is no class definition + let result = extract_config_code( + r#" + console.log("hello world"); + const message = "hello macro"; + console.debug(message); + "# + ); + assert!(!result.is_err()); + assert_eq!(result.unwrap(), None); + + // should return an error if the instance declaration is missing + let result = extract_config_code( + r#" + class LodestoneConfig { + id: string = 'defaultId'; + } + "# + ); + assert!(result.is_err()); + + // should return an error if the class definition is missing + let result = extract_config_code( + r#" + declare let config: LodestoneConfig; + console.debug(config); + "# + ); + assert!(result.is_err()); + + // should extract the correct instance name and class definition + let result = extract_config_code( + r#" + class LodestoneConfig { + id: string = 'defaultId'; + } + declare let config: LodestoneConfig; + console.debug(config); + "# + ); + assert!(!result.is_err()); + let (name, code) = result.unwrap().unwrap(); + assert_eq!( + &code, + r#"{ + id: string = 'defaultId'; + }"# + ); + assert_eq!(&name, "config"); + } + + #[test] + fn test_macro_config_single_parsing() { + // should return an error if a non-option variable does not have default value + let result = parse_config_single( + "id:string", + "", + "", + ); + assert!(result.is_err()); + + // should return an error if the value and type does not match + let result = parse_config_single( + "id:number='defaultId'", + "", + "prefix", + ); + assert!(result.is_err()); + + // should properly parse the optional variable + let result = parse_config_single( + "id?:string", + "", + "prefix", + ); + let (name, config) = result.unwrap(); + assert_eq!(&name, "id"); + assert!(config.get_value().is_none()); + assert_eq!(config.get_identifier(), "prefix|id"); + + // should properly parse the non-optional variable + let result = parse_config_single( + "id:string='defaultId'", + "", + "prefix", + ); + let (name, config) = result.unwrap(); + assert_eq!(&name, "id"); + let value = config.get_value().unwrap(); + match value { + ConfigurableValue::String(val) => assert_eq!(val, "defaultId"), + _ => panic!("incorrect value") + } + assert_eq!(config.get_identifier(), "prefix|id"); + } + + #[test] + fn test_macro_config_multi_parsing() { + let result = get_config_from_code( + "config", + r#"{ + id: string = 'defaultId'; + interval?: number; + }"# + ).unwrap(); + let identifiers = ["config|id", "config|interval"]; + let configs: Vec<_> = result.iter().collect(); + for (_, settings) in configs { + assert_ne!(identifiers.iter().find(|&val| val == settings.get_identifier()), None); + } + } } mod deno_errors { From 00443a7ee102f9479903ef3aa8dc053c1540e930 Mon Sep 17 00:00:00 2001 From: Terry Zha Date: Mon, 31 Jul 2023 23:33:01 -0400 Subject: [PATCH 9/9] minor fix to the test cases --- core/src/macro_executor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index cf0e4ec6..f37f7504 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -1039,7 +1039,7 @@ mod tests { #[test] fn test_macro_config_extraction() { - // should return None if no there is no class definition + // should return None if no there is no config definition let result = extract_config_code( r#" console.log("hello world"); @@ -1047,7 +1047,7 @@ mod tests { console.debug(message); "# ); - assert!(!result.is_err()); + assert!(result.is_ok()); assert_eq!(result.unwrap(), None); // should return an error if the instance declaration is missing @@ -1079,7 +1079,7 @@ mod tests { console.debug(config); "# ); - assert!(!result.is_err()); + assert!(result.is_ok()); let (name, code) = result.unwrap().unwrap(); assert_eq!( &code,