Skip to content

Commit

Permalink
Merge pull request #339 from Lodestone-Team/macro_config_routes
Browse files Browse the repository at this point in the history
Macro config routes & UI
  • Loading branch information
CheatCod committed Jun 15, 2024
2 parents 4c43a94 + e5605c8 commit f75a36e
Show file tree
Hide file tree
Showing 18 changed files with 707 additions and 110 deletions.
5 changes: 5 additions & 0 deletions core/bindings/GetConfigResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ErrorKind } from "./ErrorKind";
import type { SettingManifest } from "./SettingManifest";

export interface GetConfigResponse { config: Record<string, SettingManifest>, message: string | null, error: ErrorKind | null, }
107 changes: 95 additions & 12 deletions core/src/handlers/instance_macro.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use axum::{
extract::Path,
routing::{get, put},
routing::{get, put, post},
Json, Router,
};

use axum_auth::AuthBearer;
use color_eyre::eyre::eyre;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use ts_rs::TS;

use crate::{
auth::user::UserAction,
Expand All @@ -16,6 +19,15 @@ use crate::{
types::InstanceUuid,
AppState,
};
use crate::traits::t_configurable::manifest::SettingManifest;

#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct GetConfigResponse {
pub config: IndexMap<String, SettingManifest>,
pub message: Option<String>,
pub error: Option<ErrorKind>,
}

pub async fn get_instance_task_list(
axum::extract::State(state): axum::extract::State<AppState>,
Expand Down Expand Up @@ -74,17 +86,33 @@ pub async fn run_macro(
kind: ErrorKind::NotFound,
source: eyre!("Instance not found"),
})?;
instance
.run_macro(
&macro_name,
args,
CausedBy::User {
user_id: requester.uid,
user_name: requester.username,
},
)
.await?;
Ok(Json(()))

if let Ok(valid_config) = instance.validate_local_config(&macro_name, None).await {
let valid_config = if valid_config.is_empty() {
None
} else {
Some(valid_config)
};

instance
.run_macro(
&macro_name,
args,
valid_config,
CausedBy::User {
user_id: requester.uid,
user_name: requester.username,
},
)
.await?;

Ok(Json(()))
} else {
Err(Error {
kind: ErrorKind::Internal,
source: eyre!("Config error"),
})
}
}

pub async fn kill_macro(
Expand All @@ -102,11 +130,66 @@ pub async fn kill_macro(
Ok(Json(()))
}

pub async fn get_macro_configs(
Path((uuid, macro_name)): Path<(InstanceUuid, String)>,
axum::extract::State(state): axum::extract::State<AppState>,
AuthBearer(token): AuthBearer,
) -> Result<Json<GetConfigResponse>, Error> {
let requester = state.users_manager.read().await.try_auth_or_err(&token)?;
requester.try_action(&UserAction::AccessMacro(Some(uuid.clone())))?;

let instance = state.instances.get(&uuid).ok_or_else(|| Error {
kind: ErrorKind::NotFound,
source: eyre!("Instance not found"),
})?;

let mut config = instance.get_macro_config(&macro_name).await?;

match instance.validate_local_config(&macro_name, Some(&config)).await {
Ok(local_value) => {
local_value.iter().for_each(|(setting_id, local_cache)| {
config[setting_id].set_optional_value(local_cache.get_value().clone()).unwrap();
});
Ok(Json(GetConfigResponse{ config, message: None, error: None }))
},
Err(e) => {
match e.kind {
ErrorKind::NotFound => {
Ok(Json(GetConfigResponse { config, message: Some("Local config cache not found".to_string()), error: Some(ErrorKind::NotFound) }))
},
_ => {
Ok(Json(GetConfigResponse { config, message: Some("There is a mismatch between a config type and its locally-stored value".to_string()), error: Some(ErrorKind::Internal) }))
}
}
},
}
}

pub async fn store_config_to_local(
Path((uuid, macro_name)): Path<(InstanceUuid, String)>,
axum::extract::State(state): axum::extract::State<AppState>,
AuthBearer(token): AuthBearer,
Json(config_to_store): Json<IndexMap<String, SettingManifest>>,
) -> Result<(), Error> {
let requester = state.users_manager.read().await.try_auth_or_err(&token)?;
requester.try_action(&UserAction::AccessMacro(Some(uuid.clone())))?;

let instance = state.instances.get(&uuid).ok_or_else(|| Error {
kind: ErrorKind::NotFound,
source: eyre!("Instance not found"),
})?;

instance.store_macro_config_to_local(&macro_name, &config_to_store).await?;
Ok(())
}

pub fn get_instance_macro_routes(state: AppState) -> Router {
Router::new()
.route("/instance/:uuid/macro/run/:macro_name", put(run_macro))
.route("/instance/:uuid/macro/kill/:pid", put(kill_macro))
.route("/instance/:uuid/macro/list", get(get_instance_macro_list))
.route("/instance/:uuid/macro/config/get/:macro_name", get(get_macro_configs))
.route("/instance/:uuid/macro/config/store/:macro_name", post(store_config_to_local))
.route("/instance/:uuid/task/list", get(get_instance_task_list))
.route(
"/instance/:uuid/history/list",
Expand Down
3 changes: 3 additions & 0 deletions core/src/implementations/generic/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use std::rc::Rc;

use async_trait::async_trait;
use color_eyre::eyre::eyre;
use indexmap::IndexMap;

use crate::error::{Error, ErrorKind};
use crate::events::CausedBy;
use crate::macro_executor::{self, WorkerOptionGenerator};
use crate::traits::t_configurable::manifest::SettingLocalCache;
use crate::traits::t_macro::{HistoryEntry, MacroEntry, TMacro, TaskEntry};

use super::bridge::procedure_call::{
Expand Down Expand Up @@ -67,6 +69,7 @@ impl TMacro for GenericInstance {
&self,
_name: &str,
_args: Vec<String>,
_configs: Option<IndexMap<String, SettingLocalCache>>,
_caused_by: CausedBy,
) -> Result<TaskEntry, Error> {
Err(Error {
Expand Down
3 changes: 3 additions & 0 deletions core/src/implementations/generic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ impl GenericInstance {
CausedBy::System,
Box::new(GenericMainWorkerGenerator::new(procedure_bridge.clone())),
None,
None,
Some(dot_lodestone_config.uuid().clone()),
)
.await?;
Expand Down Expand Up @@ -180,6 +181,7 @@ impl GenericInstance {
CausedBy::System,
Box::new(GenericMainWorkerGenerator::new(procedure_bridge.clone())),
None,
None,
Some(dot_lodestone_config.uuid().clone()),
)
.await?;
Expand Down Expand Up @@ -240,6 +242,7 @@ impl GenericInstance {
}),
None,
None,
None,
)
.await?;

Expand Down
118 changes: 118 additions & 0 deletions core/src/implementations/minecraft/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ use std::path::{Path, PathBuf};

use async_trait::async_trait;
use color_eyre::eyre::{eyre, Context};
use indexmap::IndexMap;

use crate::{
error::Error,
events::CausedBy,
macro_executor::{DefaultWorkerOptionGenerator, MacroPID, SpawnResult},
traits::t_macro::{HistoryEntry, MacroEntry, TMacro, TaskEntry},
};
use crate::error::ErrorKind;
use crate::macro_executor::MacroExecutor;
use crate::traits::t_configurable::manifest::{
SettingLocalCache,
SettingManifest,
ConfigurableValue,
};

use super::MinecraftInstance;

Expand Down Expand Up @@ -111,18 +119,51 @@ impl TMacro for MinecraftInstance {
&self,
name: &str,
args: Vec<String>,
configs: Option<IndexMap<String, SettingLocalCache>>,
caused_by: CausedBy,
) -> Result<TaskEntry, Error> {
let path_to_macro = resolve_macro_invocation(&self.path_to_macros, name)
.ok_or_else(|| eyre!("Failed to resolve macro invocation for {}", name))?;

// compose config injection code
let config_code = match configs {
Some(config_map) => {
let tokens: Vec<_> = config_map.get_index(0).unwrap().1.get_identifier().split('|').collect();
let config_var_name = tokens[0];
let mut code_string = format!("let {config_var_name} = {{\r\n");

for (var_name, meta) in config_map {
let value_code = match meta.get_value() {
Some(val) => match val {
ConfigurableValue::String(str_val) => format!("\'{str_val}\'"),
ConfigurableValue::Enum(str_val) => format!("\'{str_val}\'"),
ConfigurableValue::Boolean(b_val) => b_val.to_string(),
ConfigurableValue::Float(num) => num.to_string(),
_ => return Err(Error{
kind: ErrorKind::Internal,
source: eyre!("Unsupported config data type"),
}),
},
None => "undefined".to_string(),
};
code_string.push_str(&format!(" {var_name}: {value_code},\r\n"))
}

code_string.push_str("};\r\n");

Some(code_string)
},
None => None,
};

let SpawnResult { macro_pid: pid, .. } = self
.macro_executor
.spawn(
path_to_macro,
args,
caused_by,
Box::new(DefaultWorkerOptionGenerator),
config_code,
None,
Some(self.uuid.clone()),
)
Expand All @@ -148,4 +189,81 @@ impl TMacro for MinecraftInstance {
self.macro_executor.abort_macro(pid)?;
Ok(())
}

async fn get_macro_config(&self, name: &str) -> Result<IndexMap<String, SettingManifest>, Error> {
let path_to_macro = resolve_macro_invocation(&self.path_to_macros, name)
.ok_or_else(|| eyre!("Failed to resolve macro invocation for {}", name))?;
MacroExecutor::get_config_manifest(&path_to_macro).await
}

async fn store_macro_config_to_local(
&self,
name: &str,
config_to_store: &IndexMap<String, SettingManifest>,
) -> Result<(), Error> {
let mut local_configs: IndexMap<String, SettingLocalCache> = IndexMap::new();
config_to_store.iter().for_each(|(var_name, config)| {
local_configs.insert(var_name.clone(), SettingLocalCache::from(config));
});

let config_file_path = self.path_to_macros.join(name).join(format!("{name}_config")).with_extension("json");
std::fs::write(
config_file_path,
serde_json::to_string_pretty(&local_configs).unwrap(),
).context("failed to create the config file")?;

Ok(())
}

async fn validate_local_config(
&self,
name: &str,
config_to_validate: Option<&IndexMap<String, SettingManifest>>,
) -> Result<IndexMap<String, SettingLocalCache>, Error> {
let path_to_macro = resolve_macro_invocation(&self.path_to_macros, name)
.ok_or_else(|| eyre!("Failed to resolve macro invocation for {}", name))?;

let is_config_needed = match config_to_validate {
None => std::fs::read_to_string(path_to_macro).context("failed to read macro file")?.contains("LodestoneConfig"),
Some(_) => true,
};

// if the macro does not need a config, pass the validation ("vacuously true")
if !is_config_needed {
return Ok(IndexMap::new());
}

let config_file_path = self.path_to_macros.join(name).join(format!("{name}_config")).with_extension("json");
match std::fs::read_to_string(config_file_path) {
Ok(config_string) => {
let local_configs: IndexMap<String, SettingLocalCache> = serde_json::from_str(&config_string)
.context("failed to parse local config cache")?;

let configs = match config_to_validate {
Some(config) => config.clone(),
None => self.get_macro_config(name).await?,
};

let validation_result = local_configs.iter().fold(local_configs.len() == configs.len(), |partial_result, (setting_id, local_cache)| {
if !partial_result {
return false;
}
local_cache.validate_type(configs.get(setting_id))
});

if !validation_result {
return Err(Error {
kind: ErrorKind::Internal,
source: eyre!("There is a mismatch between a config type and its locally-stored value"),
});
}

Ok(local_configs)
},
Err(_) => Err(Error {
kind: ErrorKind::NotFound,
source: eyre!("Local config cache is not found"),
}),
}
}
}
1 change: 1 addition & 0 deletions core/src/implementations/minecraft/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl TServer for MinecraftInstance {
CausedBy::System,
Box::new(DefaultWorkerOptionGenerator),
None,
None,
Some(self.uuid.clone()),
)
.await;
Expand Down
Loading

0 comments on commit f75a36e

Please sign in to comment.