fblog 4.15.0

json log viewer
use crate::log_settings::LogSettings;
use lazy_static::lazy_static;
use mlua::{Error as LuaError, Lua};
use regex::Regex;
use serde_json::{Map, Value};
use std::fmt::Write as _;

lazy_static! {
	static ref LUA_IDENTIFIER_CLEANUP: Regex = Regex::new(r"[^A-Za-z_]").unwrap();
	static ref LUA_STRING_ESCAPE: Regex = Regex::new(r"([\n])").unwrap();
}

pub fn show_log_entry(log_entry: &Map<String, Value>, filter_expr: &str, implicit_return: bool, log_settings: &LogSettings) -> Result<bool, LuaError> {
	let lua = Lua::new();

	let script = object_to_record(log_entry, false);
	if log_settings.print_lua {
		println!("{script}");
	}
	lua.load(&script).exec()?;

	if implicit_return {
		lua.load(format!("return {filter_expr};")).eval::<bool>()
	} else {
		lua.load(filter_expr).eval::<bool>()
	}
}

fn object_to_record(object: &Map<String, Value>, nested: bool) -> String {
	let lines: Vec<String> = object
		.iter()
		.map(|(key, value)| {
			let mut script = String::new();
			let key_name = LUA_IDENTIFIER_CLEANUP.replace_all(key, "_");
			write!(script, "{key_name} = ").expect("Should be able to write to string");
			match value {
				Value::String(string_value) => write!(script, "\"{}\"", escape_lua_string(string_value)).expect("Should be able to write to string"),
				Value::Bool(bool_value) => script.push_str(&bool_value.to_string()),
				Value::Number(number_value) => script.push_str(&number_value.to_string()),
				Value::Object(nested_object) => {
					let object_string = object_to_record(nested_object, true);
					write!(script, "{{{object_string}}}").expect("Should be able to write to string");
				}
				Value::Array(array_values) => {
					let mut values = vec![];

					for array_value in array_values.iter() {
						let lua_array_value = match array_value {
							Value::String(string_value) => format!("\"{}\"", escape_lua_string(string_value)),
							Value::Bool(bool_value) => bool_value.to_string(),
							Value::Number(number_value) => number_value.to_string(),
							_ => "\"unsupported\"".to_string(),
						};

						values.push(lua_array_value);
					}

					write!(script, "{{{}}}", values.join(",")).expect("Should be able to write to string");
				}
				_ => write!(script, "\"unsupported\"").expect("Should be able to write to string"),
			}
			script
		})
		.collect();
	lines.join(if nested { "," } else { "\n" })
}

fn escape_lua_string(src: &str) -> String {
	let mut escaped = String::with_capacity(src.len());
	for c in src.chars() {
		match c {
			'\n' => escaped += "\\n",
			'\r' => escaped += "\\r",
			'\t' => escaped += "\\t",
			'"' => escaped += "\\\"",
			'\'' => escaped += "\\'",
			'\\' => escaped += "\\\\",
			c => write!(escaped, "{c}").expect("Should be able to write to string"),
		}
	}
	escaped
}

#[cfg(test)]
mod tests {
	use super::*;

	fn test_log_entry() -> Map<String, Value> {
		let mut map = Map::new();
		map.insert("message".to_string(), Value::String("something happened".to_string()));
		map.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
		map.insert("process".to_string(), Value::String("rust".to_string()));
		map.insert("fu".to_string(), Value::String("bower".to_string()));
		map.insert("level".to_string(), Value::String("info".to_string()));

		let mut nested = Map::new();
		nested.insert("log.level".to_string(), Value::String("debug".to_string()));

		map.insert("nested".to_string(), Value::Object(nested));

		let mut nested_with_array = Map::new();
		nested_with_array.insert(
			"array".to_string(),
			Value::Array(vec![
				Value::String("a".to_string()),
				Value::String("b".to_string()),
				Value::String("c".to_string()),
			]),
		);

		map.insert("nested_with_array".to_string(), Value::Object(nested_with_array));

		map
	}

	#[test]
	fn allow_all() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, "true", true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn deny_all() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(!show_log_entry(&log_entry, "false", true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn filter_process() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, r#"process == "rust""#, true, &LogSettings::new_default_settings()).unwrap());
		assert!(!show_log_entry(&log_entry, r#"process == "meep""#, true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn filter_logical_operators() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, r#"process == "rust" and fu == "bower""#, true, &LogSettings::new_default_settings()).unwrap());
		assert!(show_log_entry(&log_entry, r#"process == "rust" or fu == "bauer""#, true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn filter_contains() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(
			show_log_entry(
				&log_entry,
				r#"string.find(message, "something") ~= nil"#,
				true,
				&LogSettings::new_default_settings()
			)
			.unwrap()
		);
		assert!(!show_log_entry(&log_entry, r#"string.find(message, "bla") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn filter_regex() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, r#"string.find(fu, "bow.*") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap());
		assert!(!show_log_entry(&log_entry, r#"string.find(fu, "bow.*sd") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn unknown_variable() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(
			!show_log_entry(
				&log_entry,
				r#"sdkfjsdfjsf ~= nil and string.find(sdkfjsdfjsf, "bow.*") ~= nil"#,
				true,
				&LogSettings::new_default_settings()
			)
			.unwrap()
		);
	}

	#[test]
	fn no_implicit_return() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(
			show_log_entry(
				&log_entry,
				r#"if 3 > 2 then return true else return false end"#,
				false,
				&LogSettings::new_default_settings()
			)
			.unwrap()
		);
		assert!(
			!show_log_entry(
				&log_entry,
				r#"if 1 > 2 then return true else return false end"#,
				false,
				&LogSettings::new_default_settings()
			)
			.unwrap()
		);
	}

	#[test]
	fn neted() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, r#"nested.log_level == "debug""#, true, &LogSettings::new_default_settings()).unwrap());
	}

	#[test]
	fn nested_with_array() {
		let log_entry: Map<String, Value> = test_log_entry();
		assert!(show_log_entry(&log_entry, r#"nested_with_array.array[2] == "b""#, true, &LogSettings::new_default_settings()).unwrap());
	}
}