use std::{
fmt::Debug,
sync::{Arc, atomic::Ordering},
};
use minijinja::{
Environment, State, Value, context,
value::{Object, ObjectRepr},
};
use rand::{Rng as _, RngCore as _};
use uuid::Uuid;
use crate::deceit::DeceitResponseContext;
pub struct MiniJinjaResponseContext {
ctx: DeceitResponseContext,
}
impl Debug for MiniJinjaResponseContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MiniJinjaResponseContext")
.field("ctx", &"!debug_not_supported!")
.finish()
}
}
impl MiniJinjaResponseContext {
pub fn new(ctx: DeceitResponseContext) -> Self {
Self { ctx }
}
}
impl Object for MiniJinjaResponseContext {
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Plain
}
fn get_value(self: &Arc<Self>, field: &Value) -> Option<Value> {
match field.as_str()? {
"method" => Some(Value::from(self.ctx.req.method.as_str())),
"path" => Some(Value::from(self.ctx.req.path.as_str())),
_ => None,
}
}
fn call_method(
self: &Arc<Self>,
_state: &State<'_, '_>,
method: &str,
args: &[Value],
) -> Result<Value, minijinja::Error> {
match method {
"load_headers" => Ok(Value::from(self.ctx.req.headers.as_ref().clone())),
"load_query_args" => Ok(Value::from(self.ctx.req.query_args.as_ref().clone())),
"load_path_args" => Ok(Value::from(self.ctx.req.path_args.as_ref().clone())),
"load_body_string" => {
if self.ctx.req.body.trim_ascii().is_empty() {
Ok(Value::default())
} else {
let body = String::from_utf8_lossy(self.ctx.req.body.as_ref());
Ok(Value::from(body))
}
}
"load_body_json" => match self.ctx.req.load_body_as_json() {
Ok(v) => Ok(Value::from_serialize(v.as_ref())),
Err(e) => {
log::error!("Can't parse response body as JSON: {e}");
Err(minijinja::Error::from(minijinja::ErrorKind::CannotUnpack))
}
},
"inc_counter" => {
if args.len() != 1 {
return Err(minijinja::Error::from(
minijinja::ErrorKind::MissingArgument,
));
}
let Some(key) = args[0].as_str() else {
return Err(minijinja::Error::from(minijinja::ErrorKind::NonKey));
};
self.ctx
.counters
.get_and_increment(key)
.map(Value::from)
.map_err(|e| {
minijinja::Error::new(
minijinja::ErrorKind::UndefinedError,
format!("Can't get counter value for key \"{key}\". {e:?}"),
)
})
}
"set_response_code" => {
if args.len() != 1 {
return Err(minijinja::Error::from(
minijinja::ErrorKind::MissingArgument,
));
}
let Some(code) = args[0].as_i64() else {
return Err(minijinja::Error::from(minijinja::ErrorKind::NonPrimitive));
};
self.ctx.response_code.store(code as u16, Ordering::Relaxed);
Ok(Value::default())
}
_ => Err(minijinja::Error::from(minijinja::ErrorKind::UnknownMethod)),
}
}
}
pub fn build_tpl_context(ctx: DeceitResponseContext) -> minijinja::Value {
let mjctx = MiniJinjaResponseContext::new(ctx);
context! {
ctx => Value::from_object(mjctx)
}
}
#[derive(Default, Clone)]
pub struct MiniJinjaState {
env: Arc<std::sync::RwLock<Option<Environment<'static>>>>,
}
impl MiniJinjaState {
pub fn get_minijinja(&self) -> Environment<'static> {
self.init_minijinja_if_not();
let read_guard = self.env.read().expect("RwLock failed");
read_guard.clone().expect("Minijinja must exists here")
}
pub fn add_minijinja_template(&self, name: &str, source: &str) -> Result<(), minijinja::Error> {
self.init_minijinja_if_not();
let mut write_guard = self.env.write().expect("RwLock failed");
let env = write_guard
.as_mut()
.expect("Minijinja env must exists here");
let tpl = env.get_template(name);
if tpl.is_err() {
env.add_template_owned(name.to_string(), source.to_string())
} else {
Ok(())
}
}
fn init_minijinja_if_not(&self) {
let read_guard = self.env.read().expect("RwLock failed");
if read_guard.is_none() {
drop(read_guard);
let mut write_guard = self.env.write().expect("RwLock failed");
if write_guard.is_none() {
*write_guard = Some(init_minijinja());
}
}
}
pub fn clear(&self) {
let mut write_guard = self.env.write().expect("Write RwLock failed");
*write_guard = None;
}
}
pub(crate) fn init_minijinja() -> minijinja::Environment<'static> {
let mut env = minijinja::Environment::new();
add_clean_functions(&mut env);
env
}
pub fn add_clean_functions(env: &mut minijinja::Environment) {
env.add_function("random_num", ctx_random_num);
env.add_function("random_hex", ctx_random_hex);
env.add_function("uuid_v4", ctx_uuid_v4);
}
fn ctx_random_num(a: Option<u128>, b: Option<u128>) -> String {
let Some(first) = a else {
return rand::random::<u128>().to_string();
};
let Some(second) = b else {
let num = rand::random::<u128>() % first;
return num.to_string();
};
rand::rng().random_range(first..second).to_string()
}
fn ctx_random_hex(length: Option<u64>) -> String {
let bytes_num = length.unwrap_or(32) as usize;
let mut bytes = vec![0u8; bytes_num];
rand::rng().fill_bytes(&mut bytes);
hex::encode(bytes)
}
fn ctx_uuid_v4() -> String {
Uuid::new_v4().to_string()
}