use std::collections::HashMap; use itertools::Itertools; use quickjs_rusty::serde::from_js; use quickjs_rusty::utils::create_null; use quickjs_rusty::Context; use quickjs_rusty::ContextError; use quickjs_rusty::ExecutionError; use quickjs_rusty::JsFunction; use quickjs_rusty::OwnedJsValue as JsValue; use quickjs_rusty::ValueError; use serde::Deserialize; use serde::Serialize; #[derive(thiserror::Error, Debug)] pub enum ScriptError { #[error("error context: {0:?}")] ContextError(#[from] ContextError), #[error("error running: {0:?}")] ExecutionError(#[from] ExecutionError), #[error("error from anyhow: {0:?}")] SerdeError(#[from] quickjs_rusty::serde::Error), #[error("error value: {0:?}")] ValueError(#[from] ValueError), } pub type ScriptResult = Result; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BotFunction { func: FunctionMarker, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum FunctionMarker { /// serde is not able to (de)serialize this, so ignore it and fill /// in runtime with injection in DeserializeJS #[serde(skip)] Function(JsFunction), StrTemplate(String), } impl FunctionMarker { pub fn as_str_template(&self) -> Option<&String> { if let Self::StrTemplate(v) = self { Some(v) } else { None } } pub fn as_function(&self) -> Option<&JsFunction> { if let Self::Function(v) = self { Some(v) } else { None } } pub fn set_js_function(&mut self, f: JsFunction) { *self = Self::Function(f) } } impl BotFunction { pub fn by_name(name: String) -> Self { Self { func: FunctionMarker::StrTemplate(name), } } pub fn call_context(&self, runner: &Runner) -> ScriptResult { let func_name: &str = self .func .as_str_template() .map(|o| o.as_str()) .unwrap_or(""); runner.run_script(&format!("{func_name}()")) } pub fn set_js_function(&mut self, f: JsFunction) { self.func.set_js_function(f); } } pub trait DeserializeJS { fn js_into<'a, T: Deserialize<'a>>(&'a self) -> ScriptResult; } impl DeserializeJS for JsValue { fn js_into<'a, T: Deserialize<'a>>(&'a self) -> ScriptResult { let rc = from_js(self.context(), self)?; Ok(rc) } } #[derive(Default)] pub struct DeserializerJS { fn_map: HashMap, } impl DeserializerJS { pub fn new() -> Self { Self { fn_map: HashMap::new(), } } pub fn deserialize_js<'a, T: Deserialize<'a>>(value: &'a JsValue) -> ScriptResult { let mut s = Self::new(); s.inject_templates(value, "".to_string())?; let res = value.js_into()?; // val.map_functions(s.fn_map); Ok(res) } pub fn inject_templates( &mut self, value: &JsValue, path: String, ) -> ScriptResult> { if let Ok(f) = value.clone().try_into_function() { self.fn_map.insert(path.clone(), f); return Ok(Some(path)); } else if let Ok(o) = value.clone().try_into_object() { let path = if path.is_empty() { path } else { path + "." }; // trying to avoid . in the start // of stringified path let res = o .properties_iter()? .chunks(2) .into_iter() // since chunks(2) is used and properties iterator over object // always has even elements, unwrap will not fail .map( #[allow(clippy::unwrap_used)] |mut chunk| (chunk.next().unwrap(), chunk.next().unwrap()), ) .map(|(k, p)| k.and_then(|k| p.map(|p| (k, p)))) .filter_map(|m| m.ok()) .try_for_each(|(k, p)| { let k = match k.to_string() { Ok(k) => k, Err(err) => return Err(ScriptError::ValueError(err)), }; let res = match self.inject_templates(&p, path.clone() + &k)? { Some(_) => o.set_property(&k, JsValue::new(o.context(), create_null())), None => Ok(()), }; match res { Ok(res) => Ok(res), Err(err) => Err(ScriptError::ExecutionError(err)), } }); res?; }; Ok(None) } } // TODO: remove this function since it is suitable only for early development #[allow(clippy::print_stdout)] fn print(s: String) { println!("{s}"); } #[derive(Serialize, Deserialize, Debug)] pub struct BotConfig { version: f64, } pub trait ResolveValue { type Value; fn resolve(self, runner: &Runner) -> ScriptResult; } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum KeyboardDefinition { Rows(Vec), Function(BotFunction), } impl ResolveValue for KeyboardDefinition { type Value = Vec<::Value>; fn resolve(self, runner: &Runner) -> ScriptResult { match self { KeyboardDefinition::Rows(rows) => rows.into_iter().map(|r| r.resolve(runner)).collect(), KeyboardDefinition::Function(f) => { ::resolve(f.call_context(runner)?.js_into()?, runner) } } } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum RowDefinition { Buttons(Vec), Function(BotFunction), } impl ResolveValue for RowDefinition { type Value = Vec<::Value>; fn resolve(self, runner: &Runner) -> ScriptResult { match self { RowDefinition::Buttons(buttons) => { buttons.into_iter().map(|b| b.resolve(runner)).collect() } RowDefinition::Function(f) => { ::resolve(f.call_context(runner)?.js_into()?, runner) } } } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum ButtonDefinition { Button(ButtonRaw), ButtonLiteral(String), Function(BotFunction), } impl ResolveValue for ButtonDefinition { type Value = ButtonRaw; fn resolve(self, runner: &Runner) -> ScriptResult { match self { ButtonDefinition::Button(button) => Ok(button), ButtonDefinition::ButtonLiteral(l) => Ok(ButtonRaw::from_literal(l)), ButtonDefinition::Function(f) => { ::resolve(f.call_context(runner)?.js_into()?, runner) } } } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ButtonRaw { name: ButtonName, callback_name: String, } impl ButtonRaw { pub fn from_literal(literal: String) -> Self { ButtonRaw { name: ButtonName::Literal { literal: literal.clone(), }, callback_name: literal, } } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum ButtonName { Value { name: String }, Literal { literal: String }, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Button { name: String, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BotMessage { // buttons: Vec