diff --git a/Cargo.lock b/Cargo.lock index 2e055af62..b7519e3e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3574,6 +3574,7 @@ version = "0.0.0" dependencies = [ "daedalus", "futures", + "regex", "serde", "serde_json", "tauri", diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index c55e7fbdd..bb25c5ab0 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -6,11 +6,13 @@ authors = ["you"] license = "" repository = "" edition = "2021" +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] tauri-build = { version = "1.2", features = [] } +regex = "1.5" [dependencies] theseus = { path = "../../theseus" } diff --git a/theseus_gui/src-tauri/build.rs b/theseus_gui/src-tauri/build.rs index d860e1e6a..3ca95448b 100644 --- a/theseus_gui/src-tauri/build.rs +++ b/theseus_gui/src-tauri/build.rs @@ -1,3 +1,111 @@ +use std::fs; + +use regex::Regex; + fn main() { - tauri_build::build() + // Build the Tauri app + tauri_build::build(); + + // Check that all JavaScript 'invoke' Tauri functions have a corresponding tagged Rust function + // This is to prevent the app from crashing if a JavaScript function is invoked but the corresponding Rust function is not tagged + // This only allows simple functions, but functions in theseus_gui should be kept simple + check_invoke_sanity(); +} + +fn check_invoke_sanity() { + let js_files = read_js_files("../src/helpers"); + let rust_files = read_rust_files("src"); + + let js_function_names = extract_js_function_names(&js_files); + let rust_function_names = extract_rust_function_names(&rust_files); + + let mut missing_functions = Vec::new(); + for js_fn_name in js_function_names { + if !rust_function_names.contains(&js_fn_name) { + missing_functions.push(js_fn_name); + } + } + if !missing_functions.is_empty() { + panic!( + "The following invoked Tauri functions do not have corresponding Rust functions with #[tauri::command] attribute :\n{}", + missing_functions.join("\n") + ); + } +} + +fn read_js_files(directory: &str) -> Vec { + let mut files = Vec::new(); + read_files_recursively(directory, "js", &mut files); + files +} + +fn read_rust_files(directory: &str) -> Vec { + let mut files = Vec::new(); + read_files_recursively(directory, "rs", &mut files); + files +} + +// Recursive in case we make the helpers directory more complex +fn read_files_recursively( + directory: &str, + extension: &str, + files: &mut Vec, +) { + for entry in fs::read_dir(directory).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + read_files_recursively(&path.to_string_lossy(), extension, files); + } else if path.extension().map_or(false, |ext| ext == extension) { + let content = fs::read_to_string(path).unwrap(); + files.push(content); + } + } +} + +fn extract_rust_function_names(rust_files: &[String]) -> Vec { + // Matches #[tauri::command] attribute + let re_tauri_command = Regex::new(r"(?m)#\[tauri::command\]").unwrap(); + // Matches function name following the #[tauri::command] attribute + // Matches up to the first (, to allow for function arguments and comments in that area + let re_function_name = + Regex::new(r"fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap(); + let mut function_names = Vec::new(); + + for file in rust_files { + let mut start = 0; + while let Some(command_match) = re_tauri_command.find_at(file, start) { + if let Some(function_name_cap) = + re_function_name.captures(&file[command_match.end()..]) + { + function_names.push(function_name_cap[1].to_string()); + start = command_match.start() + 1; + } else { + break; + } + } + } + + function_names +} + +fn extract_js_function_names(js_files: &[String]) -> Vec { + // Matches functions of the form: invoke('function_name', { ... }) (or invoke('function_name') ) + let re_invoke = Regex::new( + r"(?m)invoke\(\s*'([a-zA-Z_][a-zA-Z0-9_]*)'\s*(?:,\s*\{.*?\})?\s*\)", + ) + .unwrap(); + let mut function_names = Vec::new(); + + for file in js_files { + let mut start = 0; + while let Some(invoke_match) = re_invoke.find_at(file, start) { + if let Some(captures) = re_invoke.captures(invoke_match.as_str()) { + function_names.push(captures[1].to_string()); + } + start = invoke_match.start() + 1; + } + } + + function_names } diff --git a/theseus_gui/src-tauri/src/api/tags.rs b/theseus_gui/src-tauri/src/api/tags.rs index e33b53aef..8748b31d7 100644 --- a/theseus_gui/src-tauri/src/api/tags.rs +++ b/theseus_gui/src-tauri/src/api/tags.rs @@ -5,38 +5,37 @@ use theseus::tags::{ /// Gets cached category tags from the database #[tauri::command] -pub async fn tags_get_category_tags() -> Result> { +pub async fn tags_get_categories() -> Result> { Ok(theseus::tags::get_category_tags().await?) } /// Gets cached report type tags from the database #[tauri::command] -pub async fn tags_get_report_type_tags() -> Result> { +pub async fn tags_get_report_types() -> Result> { Ok(theseus::tags::get_report_type_tags().await?) } /// Gets cached loader tags from the database #[tauri::command] -pub async fn tags_get_loader_tags() -> Result> { +pub async fn tags_get_loaders() -> Result> { Ok(theseus::tags::get_loader_tags().await?) } /// Gets cached game version tags from the database #[tauri::command] -pub async fn tags_get_game_version_tags() -> Result> { +pub async fn tags_get_game_versions() -> Result> { Ok(theseus::tags::get_game_version_tags().await?) } /// Gets cached license tags from the database #[tauri::command] -pub async fn tags_get_license_tags() -> Result> { +pub async fn tags_get_licenses() -> Result> { Ok(theseus::tags::get_license_tags().await?) } /// Gets cached donation platform tags from the database #[tauri::command] -pub async fn tags_get_donation_platform_tags() -> Result> -{ +pub async fn tags_get_donation_platforms() -> Result> { Ok(theseus::tags::get_donation_platform_tags().await?) } diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index 6857af988..86bb1f22a 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -36,12 +36,12 @@ fn main() { api::auth::auth_has_user, api::auth::auth_users, api::auth::auth_get_user, - api::tags::tags_get_category_tags, - api::tags::tags_get_donation_platform_tags, - api::tags::tags_get_game_version_tags, - api::tags::tags_get_loader_tags, - api::tags::tags_get_license_tags, - api::tags::tags_get_report_type_tags, + api::tags::tags_get_categories, + api::tags::tags_get_donation_platforms, + api::tags::tags_get_game_versions, + api::tags::tags_get_loaders, + api::tags::tags_get_licenses, + api::tags::tags_get_report_types, api::tags::tags_get_tag_bundle, api::settings::settings_get, api::settings::settings_set, diff --git a/theseus_gui/src/helpers/tags.js b/theseus_gui/src/helpers/tags.js index 207e2467a..6bdd36e82 100644 --- a/theseus_gui/src/helpers/tags.js +++ b/theseus_gui/src/helpers/tags.js @@ -12,17 +12,17 @@ export async function get_tag_bundle() { // Gets cached category tags export async function get_categories() { - return await invoke('tags_get_category_tags') + return await invoke('tags_get_categories') } // Gets cached loaders tags export async function get_loaders() { - return await invoke('tags_get_loader_tags') + return await invoke('tags_get_loaders') } // Gets cached game_versions tags export async function get_game_versions() { - return await invoke('tags_get_game_version_tags') + return await invoke('tags_get_game_versions') } // Gets cached licenses tags