zen-engine 0.55.0

Business rules engine
Documentation
use std::fmt::{Debug, Formatter};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;

use crate::nodes::function::v2::error::{FunctionError, FunctionResult, ResultExt};
use crate::nodes::function::v2::listener::{RuntimeEvent, RuntimeListener};
use crate::nodes::function::v2::module::console::{Console, Log};
use crate::nodes::function::v2::module::ModuleLoader;
use crate::nodes::function::v2::serde::{JsValue, JsValueWithNodes};
use rquickjs::promise::MaybePromise;
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module};
use serde::{Deserialize, Serialize};
use zen_expression::variable::{ToVariable, Variable};

pub struct FunctionConfig {
    pub(crate) listeners: Option<Vec<Box<dyn RuntimeListener>>>,
}

pub struct Function {
    rt: Arc<AsyncRuntime>,
    ctx: AsyncContext,
    listeners: Vec<Box<dyn RuntimeListener>>,
    module_loader: ModuleLoader,
}

impl Debug for Function {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Function")
    }
}

impl Function {
    pub async fn create<'js>(config: FunctionConfig) -> FunctionResult<Self> {
        let module_loader = ModuleLoader::new();
        let rt = Arc::new(AsyncRuntime::new()?);

        rt.set_loader(module_loader.clone(), module_loader.clone())
            .await;

        let ctx = AsyncContext::full(&rt).await?;
        let this = Self {
            rt,
            ctx,
            module_loader,
            listeners: config.listeners.unwrap_or_default(),
        };

        this.dispatch_event(RuntimeEvent::Startup).await?;
        Ok(this)
    }

    async fn dispatch_event_inner(&self, ctx: &Ctx<'_>, event: RuntimeEvent) -> FunctionResult {
        for listener in &self.listeners {
            if let Err(err) = listener.on_event(ctx.clone(), event.clone()).await {
                return Err(err.into());
            };
        }

        Ok(())
    }

    async fn dispatch_event(&self, event: RuntimeEvent) -> FunctionResult {
        async_with!(&self.ctx => |ctx| {
            self.dispatch_event_inner(&ctx, event).await
        })
        .await
    }

    pub fn context(&self) -> &AsyncContext {
        &self.ctx
    }

    pub fn runtime(&self) -> &AsyncRuntime {
        &self.rt
    }

    pub fn suggest_module_name<'a>(&self, name: &str, source: &str) -> String {
        let declarative_name = format!("node:{name}");

        if self.module_loader.has_module(&declarative_name) {
            let content_hash = create_content_hash(source);
            format!("node:{name}.{content_hash:x}")
        } else {
            declarative_name
        }
    }

    pub async fn register_module(&self, name: &str, source: &str) -> FunctionResult {
        let maybe_error: Option<FunctionError> = async_with!(&self.ctx => |ctx| {
            if let Err(err) = Module::declare(ctx.clone(), name.as_bytes().to_vec(), source.as_bytes().to_vec()).catch(&ctx) {
                return Some(err.into())
            }

            return None;
        }).await;
        if let Some(err) = maybe_error {
            return Err(err);
        }

        self.module_loader.add_module(name.to_string());
        Ok(())
    }

    pub(crate) async fn call_handler(
        &self,
        name: &str,
        data: JsValueWithNodes,
    ) -> FunctionResult<HandlerResponse> {
        let k: FunctionResult<HandlerResponse> = async_with!(&self.ctx => |ctx| {
            self.dispatch_event_inner(&ctx, RuntimeEvent::SoftReset).await?;

            let m: rquickjs::Object = Module::import(&ctx, name).catch(&ctx)?.into_future().await.catch(&ctx)?;
            let handler: rquickjs::Function = m.get("handler").catch(&ctx)?;

            let handler_promise: MaybePromise = handler.call((data, 5)).catch(&ctx)?;
            let handler_result = handler_promise.into_future::<JsValue>().await.catch(&ctx)?;

            let console = Console::from_context(&ctx).or_throw(&ctx)?;
            let logs = console.logs.into_inner();

            Ok(HandlerResponse { data: handler_result.0, logs })
        })
        .await;

        Ok(k?)
    }

    pub(crate) async fn extract_logs(&self) -> Vec<Log> {
        let logs: Option<Vec<Log>> = async_with!(&self.ctx => |ctx| {
            let console = Console::from_context(&ctx).ok()?;
            Some(console.logs.into_inner())
        })
        .await;

        logs.unwrap_or_default()
    }
}

#[derive(Serialize, Deserialize, ToVariable)]
pub struct HandlerResponse {
    pub logs: Vec<Log>,
    pub data: Variable,
}

fn create_content_hash(content: &str) -> u64 {
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    hasher.finish()
}