use crate::core::config::config_manager::ConfigManager;
use crate::http::http_request::HttpRequest;
use crate::http::http_response::HttpResponse;
use http::{Method, StatusCode};
use serde_json::Value;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex, MutexGuard};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WebConfigError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Validation error: {0}")]
ValidationError(String),
}
const FRONTEND_REPO_URL: &str = "https://github.com/YuFireWhisper/blur-web-config.git";
pub struct WebConfig {
path: PathBuf,
file_lock: Mutex<()>,
}
impl WebConfig {
pub fn new(path: &PathBuf) -> Result<Self, WebConfigError> {
if !path.exists() {
return Err(WebConfigError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Config file {} does not exist", path.display()),
)));
}
let content = fs::read(path)?;
let _: Value = serde_json::from_slice(&content)?;
ensure_static_up_to_date()?;
Ok(Self {
path: path.clone(),
file_lock: Mutex::new(()),
})
}
pub fn get_json(&self) -> Result<Value, WebConfigError> {
let _guard = self.file_lock.lock().unwrap();
let content = fs::read(&self.path)?;
let json: Value = serde_json::from_slice(&content)?;
Ok(json)
}
pub fn update_parameter(
&self,
json_pointer: &str,
new_value: &str,
) -> Result<(), WebConfigError> {
let _guard = self.file_lock.lock().unwrap();
let content = fs::read(&self.path)?;
let mut config: Value = serde_json::from_slice(&content)?;
self.validate_json_pointer(json_pointer)?;
let parent_pointer = &json_pointer[..json_pointer.rfind('/').unwrap()];
let parent = config.pointer_mut(parent_pointer).ok_or_else(|| {
WebConfigError::ValidationError(format!("Invalid JSON pointer: {}", parent_pointer))
})?;
self.update_json_value(parent, new_value)?;
let updated = serde_json::to_vec_pretty(&config)?;
fs::write(&self.path, updated)?;
Ok(())
}
fn validate_json_pointer(&self, json_pointer: &str) -> Result<(), WebConfigError> {
if !json_pointer.starts_with('/') {
return Err(WebConfigError::ValidationError(
"JSON Pointer must start with '/'".into(),
));
}
if !json_pointer.ends_with("/value") {
return Err(WebConfigError::ValidationError(
"JSON Pointer must end with '/value'".into(),
));
}
Ok(())
}
fn update_json_value(&self, parent: &mut Value, new_value: &str) -> Result<(), WebConfigError> {
let expected_type = parent.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
WebConfigError::ValidationError("Parameter missing 'type' field".into())
})?;
let default_val = parent.get("default").and_then(|v| v.as_str()).unwrap_or("");
let validated_value = validate_and_convert(expected_type, new_value, default_val)?;
parent
.as_object_mut()
.ok_or_else(|| WebConfigError::ValidationError("Parameter is not an object".into()))?
.insert("value".into(), Value::String(validated_value));
Ok(())
}
pub fn add_block(&self, parent_path: &str, block_name: &str) -> Result<(), WebConfigError> {
let _guard = self.file_lock.lock().unwrap();
let content = fs::read(&self.path)?;
let mut config: Value = serde_json::from_slice(&content)?;
self.add_block_to_config(&mut config, parent_path, block_name)?;
let updated = serde_json::to_vec_pretty(&config)?;
fs::write(&self.path, updated)?;
Ok(())
}
fn add_block_to_config(
&self,
config: &mut Value,
parent_path: &str,
block_name: &str,
) -> Result<(), WebConfigError> {
let parent = self.get_parent_block_mut(config, parent_path)?;
let parent_obj = self.get_parent_object_mut(parent)?;
let template = self.get_block_template(block_name)?;
self.validate_block_uniqueness(&template, block_name)?;
let children_entry = self.ensure_children_object_exists(parent_obj)?;
self.add_template_to_children(children_entry, block_name, template)?;
Ok(())
}
fn get_parent_block_mut<'a>(
&self,
config: &'a mut Value,
parent_path: &str,
) -> Result<&'a mut Value, WebConfigError> {
if parent_path.is_empty() {
Ok(config)
} else {
config.pointer_mut(parent_path).ok_or_else(|| {
WebConfigError::ValidationError(format!(
"Invalid parent JSON pointer: {}",
parent_path
))
})
}
}
fn get_parent_object_mut<'a>(
&self,
parent: &'a mut Value,
) -> Result<&'a mut serde_json::Map<String, Value>, WebConfigError> {
parent
.as_object_mut()
.ok_or_else(|| WebConfigError::ValidationError("Parent is not an object".into()))
}
fn get_block_template(&self, block_name: &str) -> Result<Value, WebConfigError> {
ConfigManager::get_block_template(block_name, true).ok_or_else(|| {
WebConfigError::ValidationError(format!("Block {} not registered", block_name))
})
}
fn validate_block_uniqueness(
&self,
template: &Value,
block_name: &str,
) -> Result<(), WebConfigError> {
if template
.get("unique")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return Err(WebConfigError::ValidationError(format!(
"Block {} is unique and cannot be added",
block_name
)));
}
Ok(())
}
fn ensure_children_object_exists<'a>(
&self,
parent_obj: &'a mut serde_json::Map<String, Value>,
) -> Result<&'a mut serde_json::Map<String, Value>, WebConfigError> {
if parent_obj.contains_key("children") {
if let Some(children_val) = parent_obj.get_mut("children") {
children_val.as_object_mut().ok_or_else(|| {
WebConfigError::ValidationError(
"Parent's 'children' field is not an object".into(),
)
})
} else {
Err(WebConfigError::ValidationError(
"Unexpected error accessing 'children' field".into(),
))
}
} else {
parent_obj.insert(
"children".to_string(),
Value::Object(serde_json::Map::new()),
);
parent_obj
.get_mut("children")
.and_then(|v| v.as_object_mut())
.ok_or_else(|| {
WebConfigError::ValidationError(
"Failed to create or access 'children' object".into(),
)
})
}
}
fn add_template_to_children(
&self,
children_entry: &mut serde_json::Map<String, Value>,
block_name: &str,
template: Value,
) -> Result<(), WebConfigError> {
if let Some(existing) = children_entry.get_mut(block_name) {
if let Some(arr) = existing.as_array_mut() {
arr.push(template);
} else {
return Err(WebConfigError::ValidationError(format!(
"The 'children' field for {} is not an array",
block_name
)));
}
} else {
children_entry.insert(block_name.to_string(), Value::Array(vec![template]));
}
Ok(())
}
pub fn delete_block(&self, block_path: &str) -> Result<(), WebConfigError> {
let _guard = self.file_lock.lock().unwrap();
let content = fs::read(&self.path)?;
let mut config: Value = serde_json::from_slice(&content)?;
self.delete_block_from_config(&mut config, block_path)?;
let updated = serde_json::to_vec_pretty(&config)?;
fs::write(&self.path, updated)?;
Ok(())
}
fn delete_block_from_config(
&self,
config: &mut Value,
block_path: &str,
) -> Result<(), WebConfigError> {
if !block_path.starts_with('/') {
return Err(WebConfigError::ValidationError(
"Block path must start with '/'".into(),
));
}
let (parent_pointer, token) = self.parse_block_path(block_path)?;
let parent = self.get_deletable_parent_mut(config, parent_pointer)?;
self.process_block_deletion(parent, token, parent_pointer)?;
Ok(())
}
fn parse_block_path<'a>(
&self,
block_path: &'a str,
) -> Result<(&'a str, &'a str), WebConfigError> {
let last_slash = block_path
.rfind('/')
.ok_or_else(|| WebConfigError::ValidationError("Invalid block path".into()))?;
let parent_pointer = if last_slash == 0 {
""
} else {
&block_path[..last_slash]
};
let token = &block_path[last_slash + 1..];
if token.is_empty() {
return Err(WebConfigError::ValidationError(
"Empty token in block path".into(),
));
}
Ok((parent_pointer, token))
}
fn get_deletable_parent_mut<'a>(
&self,
config: &'a mut Value,
parent_pointer: &str,
) -> Result<&'a mut Value, WebConfigError> {
config.pointer_mut(parent_pointer).ok_or_else(|| {
WebConfigError::ValidationError(format!(
"Invalid JSON pointer for parent: {}",
parent_pointer
))
})
}
fn process_block_deletion(
&self,
parent: &mut Value,
token: &str,
parent_pointer: &str,
) -> Result<(), WebConfigError> {
if let Some(arr) = parent.as_array_mut() {
self.remove_block_from_array(arr, token, parent_pointer)?;
} else if let Some(obj) = parent.as_object_mut() {
self.remove_block_from_object(obj, token)?;
} else {
return Err(WebConfigError::ValidationError(
"Parent is neither an array nor an object".into(),
));
}
Ok(())
}
fn remove_block_from_array(
&self,
arr: &mut Vec<Value>,
token: &str,
parent_pointer: &str,
) -> Result<(), WebConfigError> {
let index: usize = token.parse().map_err(|_| {
WebConfigError::ValidationError(format!("Invalid array index: {}", token))
})?;
if index >= arr.len() {
return Err(WebConfigError::ValidationError(format!(
"Index {} out of bounds",
index
)));
}
let block_name = if parent_pointer.is_empty() {
token
} else {
parent_pointer.rsplit('/').next().unwrap_or(token)
};
let template = self.get_block_template(block_name)?;
self.validate_block_uniqueness_deletion(&template, block_name)?;
if arr.len() == 1 {
*arr = vec![template];
} else {
arr.remove(index);
}
Ok(())
}
fn remove_block_from_object(
&self,
obj: &mut serde_json::Map<String, Value>,
token: &str,
) -> Result<(), WebConfigError> {
let block_value = obj
.get_mut(token)
.ok_or_else(|| WebConfigError::ValidationError(format!("Block {} not found", token)))?;
let template = self.get_block_template(token)?;
self.validate_block_uniqueness_deletion(&template, token)?;
if let Value::Array(arr) = block_value {
match arr.len() {
0 => Err(WebConfigError::ValidationError(format!(
"Block {} array is empty",
token
))),
1 => {
*arr = vec![template];
Ok(())
}
_ => {
arr.pop();
Ok(())
}
}
} else {
obj.insert(token.to_string(), Value::Array(vec![template]));
Ok(())
}
}
fn validate_block_uniqueness_deletion(
&self,
template: &Value,
block_name: &str,
) -> Result<(), WebConfigError> {
if template
.get("unique")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return Err(WebConfigError::ValidationError(format!(
"Unique block {} cannot be deleted",
block_name
)));
}
Ok(())
}
}
fn validate_and_convert(
expected_type: &str,
new_value: &str,
default: &str,
) -> Result<String, WebConfigError> {
let trimmed = new_value.trim();
if expected_type.eq_ignore_ascii_case("bool") {
let lower = trimmed.to_lowercase();
if lower == "true" || lower == "false" {
Ok(lower)
} else {
Err(WebConfigError::ValidationError(format!(
"Invalid boolean value: {}",
new_value
)))
}
} else if expected_type.eq_ignore_ascii_case("string") {
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
} else if expected_type.eq_ignore_ascii_case("u16")
|| expected_type.eq_ignore_ascii_case("u32")
|| expected_type.eq_ignore_ascii_case("i32")
|| expected_type.eq_ignore_ascii_case("u64")
|| expected_type.eq_ignore_ascii_case("i64")
{
if trimmed.is_empty() {
Ok(default.to_string())
} else if trimmed.parse::<i64>().is_ok() {
Ok(trimmed.to_string())
} else {
Err(WebConfigError::ValidationError(format!(
"Invalid numeric value: {}",
new_value
)))
}
} else if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
pub fn add_all_web_config_handlers(
web_config: Arc<WebConfig>,
mut proc_lock: MutexGuard<'_, crate::core::processor::HttpProcessor>,
) {
let project_root = env!("CARGO_MANIFEST_DIR");
let static_path = format!("{}/static/dist/", project_root);
proc_lock
.serve_static_at("/web_config", &static_path)
.expect("Failed to register /web_config mapping");
register_get_json_handler(&web_config, &mut proc_lock);
register_update_handler(&web_config, &mut proc_lock);
register_add_block_handler(&web_config, &mut proc_lock);
register_delete_block_handler(&web_config, &mut proc_lock);
}
fn register_get_json_handler(
web_config: &Arc<WebConfig>,
proc_lock: &mut MutexGuard<'_, crate::core::processor::HttpProcessor>,
) {
proc_lock.add_handler(
"/web_config/json".to_string(),
StatusCode::OK,
&Method::GET,
Box::new({
let wc = Arc::clone(web_config);
move |req: &HttpRequest| match wc.get_json() {
Ok(json) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::OK);
resp.set_header("Content-Type", "application/json");
resp.set_body(&serde_json::to_string_pretty(&json).unwrap_or_default());
resp
}
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(
req.version().to_owned(),
StatusCode::INTERNAL_SERVER_ERROR,
);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Error: {:?}", e));
resp
}
}
}),
);
}
fn register_update_handler(
web_config: &Arc<WebConfig>,
proc_lock: &mut MutexGuard<'_, crate::core::processor::HttpProcessor>,
) {
proc_lock.add_handler(
"/web_config/update".to_string(),
StatusCode::OK,
&Method::POST,
Box::new({
let wc = Arc::clone(web_config);
move |req: &HttpRequest| {
let body = String::from_utf8(req.body().to_vec()).unwrap_or_default();
let req_json: Value = match serde_json::from_str(&body) {
Ok(j) => j,
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Invalid JSON: {:?}", e));
return resp;
}
};
let path = req_json.get("path").and_then(|v| v.as_str()).unwrap_or("");
let new_value = req_json
.get("new_value")
.and_then(|v| v.as_str())
.unwrap_or("");
match wc.update_parameter(path, new_value) {
Ok(_) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::OK);
resp.set_header("Content-Type", "text/plain");
resp.set_body("Parameter updated");
resp
}
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Error: {:?}", e));
resp
}
}
}
}),
);
}
fn register_add_block_handler(
web_config: &Arc<WebConfig>,
proc_lock: &mut MutexGuard<'_, crate::core::processor::HttpProcessor>,
) {
proc_lock.add_handler(
"/web_config/add_block".to_string(),
StatusCode::OK,
&Method::POST,
Box::new({
let wc = Arc::clone(web_config);
move |req: &HttpRequest| {
let body = String::from_utf8(req.body().to_vec()).unwrap_or_default();
let req_json: Value = match serde_json::from_str(&body) {
Ok(j) => j,
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Invalid JSON: {:?}", e));
return resp;
}
};
let parent_path = req_json
.get("parent_path")
.and_then(|v| v.as_str())
.unwrap_or("");
let block_name = req_json
.get("block_name")
.and_then(|v| v.as_str())
.unwrap_or("");
match wc.add_block(parent_path, block_name) {
Ok(_) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::OK);
resp.set_header("Content-Type", "text/plain");
resp.set_body("Block added");
resp
}
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Error: {:?}", e));
resp
}
}
}
}),
);
}
fn register_delete_block_handler(
web_config: &Arc<WebConfig>,
proc_lock: &mut MutexGuard<'_, crate::core::processor::HttpProcessor>,
) {
proc_lock.add_handler(
"/web_config/delete_block".to_string(),
StatusCode::OK,
&Method::POST,
Box::new({
let wc = Arc::clone(web_config);
move |req: &HttpRequest| {
let body = String::from_utf8(req.body().to_vec()).unwrap_or_default();
let req_json: Value = match serde_json::from_str(&body) {
Ok(j) => j,
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Invalid JSON: {:?}", e));
return resp;
}
};
let block_path = req_json
.get("block_path")
.and_then(|v| v.as_str())
.unwrap_or("");
match wc.delete_block(block_path) {
Ok(_) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::OK);
resp.set_header("Content-Type", "text/plain");
resp.set_body("Block deleted");
resp
}
Err(e) => {
let mut resp = HttpResponse::new();
resp.set_status_line(req.version().to_owned(), StatusCode::BAD_REQUEST);
resp.set_header("Content-Type", "text/plain");
resp.set_body(&format!("Error: {:?}", e));
resp
}
}
}
}),
);
}
fn ensure_static_up_to_date() -> Result<PathBuf, WebConfigError> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let repo_dir = PathBuf::from(manifest_dir).join("static");
let dist_dir = repo_dir.join("dist");
if needs_update(&repo_dir)? {
if !repo_dir.exists() {
let status = Command::new("git")
.args(["clone", FRONTEND_REPO_URL, repo_dir.to_str().unwrap()])
.status()
.map_err(|e| {
WebConfigError::ValidationError(format!("Failed to clone repository: {}", e))
})?;
if !status.success() {
return Err(WebConfigError::ValidationError("Git clone failed".into()));
}
} else {
let status = Command::new("git")
.args(["pull"])
.current_dir(&repo_dir)
.status()
.map_err(|e| {
WebConfigError::ValidationError(format!("Failed to run git pull: {}", e))
})?;
if !status.success() {
return Err(WebConfigError::ValidationError("Git pull failed".into()));
}
}
let install_status = Command::new("npm")
.arg("install")
.current_dir(&repo_dir)
.status()
.map_err(|e| {
WebConfigError::ValidationError(format!("Failed to run npm install: {}", e))
})?;
if !install_status.success() {
return Err(WebConfigError::ValidationError("npm install failed".into()));
}
let build_status = Command::new("npm")
.arg("run")
.arg("build")
.current_dir(&repo_dir)
.status()
.map_err(|e| {
WebConfigError::ValidationError(format!("Failed to run npm run build: {}", e))
})?;
if !build_status.success() {
return Err(WebConfigError::ValidationError(
"npm run build failed".into(),
));
}
if !dist_dir.exists() {
return Err(WebConfigError::ValidationError(
"Build did not produce dist directory".into(),
));
}
}
Ok(dist_dir)
}
fn needs_update(static_dir: &Path) -> Result<bool, WebConfigError> {
if !static_dir.exists() || !static_dir.join(".git").exists() {
println!("Static files or .git directory not found, need update");
return Ok(true);
}
if !static_dir.join("dist/index.html").exists() {
println!("dist/index.html not found, need update (likely initial clone)");
return Ok(true);
}
println!("Static files found, checking git status");
let get_git_hash = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(static_dir)
.output()
.map(|output| {
if output.status.success() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
String::new()
}
})
.unwrap_or_default()
};
let head = get_git_hash(&["rev-parse", "HEAD"]);
let upstream = get_git_hash(&["rev-parse", "@{u}"]);
if head.is_empty() || upstream.is_empty() {
println!("Git commands failed (possibly no upstream), forcing update.");
return Ok(true);
}
Ok(head != upstream)
}