diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts index 4207b556..49c0445d 100644 --- a/bindings/nodejs/index.d.ts +++ b/bindings/nodejs/index.d.ts @@ -8,7 +8,7 @@ export interface ZenEvaluateOptions { trace?: boolean } export interface ZenEngineOptions { - loader?: (key: string) => Promise + loader?: (key: string) => Promise customHandler?: (request: ZenEngineHandlerRequest) => Promise } export function evaluateExpressionSync(expression: string, context?: any | undefined | null): any @@ -40,6 +40,10 @@ export interface DecisionNode { kind: string config: any } +export class ZenDecisionContent { + constructor(content: Buffer | object) + toBuffer(): Buffer +} export class ZenDecision { constructor() evaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): Promise @@ -48,7 +52,7 @@ export class ZenDecision { export class ZenEngine { constructor(options?: ZenEngineOptions | undefined | null) evaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise - createDecision(content: Buffer): ZenDecision + createDecision(content: ZenDecisionContent | Buffer | object): ZenDecision getDecision(key: string): Promise } export class ZenEngineHandlerRequest { diff --git a/bindings/nodejs/index.js b/bindings/nodejs/index.js index 15e057d2..6dfca0df 100644 --- a/bindings/nodejs/index.js +++ b/bindings/nodejs/index.js @@ -281,8 +281,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { ZenDecision, ZenEngine, evaluateExpressionSync, evaluateUnaryExpressionSync, renderTemplateSync, evaluateExpression, evaluateUnaryExpression, renderTemplate, ZenEngineHandlerRequest } = nativeBinding +const { ZenDecisionContent, ZenDecision, ZenEngine, evaluateExpressionSync, evaluateUnaryExpressionSync, renderTemplateSync, evaluateExpression, evaluateUnaryExpression, renderTemplate, ZenEngineHandlerRequest } = nativeBinding +module.exports.ZenDecisionContent = ZenDecisionContent module.exports.ZenDecision = ZenDecision module.exports.ZenEngine = ZenEngine module.exports.evaluateExpressionSync = evaluateExpressionSync diff --git a/bindings/nodejs/src/content.rs b/bindings/nodejs/src/content.rs new file mode 100644 index 00000000..ad8f65c2 --- /dev/null +++ b/bindings/nodejs/src/content.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use napi::bindgen_prelude::Buffer; +use napi::{Either, Env, JsObject}; +use napi_derive::napi; +use serde_json::Value; + +use zen_engine::model::DecisionContent; + +#[napi] +pub struct ZenDecisionContent { + pub(crate) inner: Arc, +} + +#[napi] +impl ZenDecisionContent { + #[napi(constructor)] + pub fn new(env: Env, content: Either) -> napi::Result { + let decision_content: DecisionContent = match content { + Either::A(buf) => serde_json::from_slice(buf.as_ref())?, + Either::B(obj) => { + let serde_val: Value = env.from_js_value(obj)?; + serde_json::from_value(serde_val)? + } + }; + + Ok(Self { + inner: Arc::new(decision_content), + }) + } + + #[napi] + pub fn to_buffer(&self) -> napi::Result { + let content_vec = serde_json::to_vec(&self.inner.as_ref())?; + Ok(Buffer::from(content_vec)) + } +} diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index 3a41173c..cc35012a 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -1,10 +1,11 @@ +use crate::content::ZenDecisionContent; use crate::custom_node::CustomNode; use crate::decision::ZenDecision; use crate::loader::DecisionLoader; use crate::types::ZenEngineResponse; use napi::anyhow::{anyhow, Context}; -use napi::bindgen_prelude::Buffer; -use napi::{tokio, Env, JsFunction}; +use napi::bindgen_prelude::{Buffer, Either3}; +use napi::{tokio, Env, JsFunction, JsObject}; use napi_derive::napi; use serde_json::Value; use std::sync::Arc; @@ -33,7 +34,7 @@ impl Default for ZenEvaluateOptions { #[napi(object)] pub struct ZenEngineOptions { - #[napi(ts_type = "(key: string) => Promise")] + #[napi(ts_type = "(key: string) => Promise")] pub loader: Option, #[napi(ts_type = "(request: ZenEngineHandlerRequest) => Promise")] @@ -99,9 +100,21 @@ impl ZenEngine { } #[napi] - pub fn create_decision(&self, content: Buffer) -> napi::Result { - let decision_content: DecisionContent = serde_json::from_slice(content.as_ref())?; - let decision = self.graph.create_decision(Arc::new(decision_content)); + pub fn create_decision( + &self, + env: Env, + content: Either3<&ZenDecisionContent, Buffer, JsObject>, + ) -> napi::Result { + let decision_content: Arc = match content { + Either3::A(c) => c.inner.clone(), + Either3::B(buffer) => Arc::new(serde_json::from_slice(buffer.as_ref())?), + Either3::C(obj) => { + let serde_val: Value = env.from_js_value(obj)?; + Arc::new(serde_json::from_value(serde_val)?) + } + }; + + let decision = self.graph.create_decision(decision_content); Ok(ZenDecision::from(decision)) } diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 4bcead12..4029fc54 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -1,3 +1,4 @@ +mod content; mod custom_node; mod decision; mod engine; diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index d493b887..3e76ed70 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -1,24 +1,21 @@ +use std::sync::Arc; + use async_trait::async_trait; use napi::anyhow::anyhow; use napi::bindgen_prelude::{Buffer, Promise}; use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; -use napi::{Env, JsFunction}; - -use std::sync::Arc; +use napi::{Either, Env, JsFunction}; use zen_engine::loader::{DecisionLoader as DecisionLoaderTrait, LoaderError, LoaderResult}; use zen_engine::model::DecisionContent; +use crate::content::ZenDecisionContent; + +#[derive(Default)] pub(crate) struct DecisionLoader { function: Option>, } -impl Default for DecisionLoader { - fn default() -> Self { - Self { function: None } - } -} - impl DecisionLoader { pub fn try_new(env: &mut Env, function: JsFunction) -> napi::Result { let mut tsf = @@ -42,14 +39,14 @@ impl DecisionLoader { .into()); }; - let promise: Promise> = function + let promise: Promise>> = function .clone() .call_async(key.to_string()) .await .map_err(|e| LoaderError::Internal { - key: key.to_string(), - source: anyhow!(e.reason), - })?; + key: key.to_string(), + source: anyhow!(e.reason), + })?; let result = promise.await.map_err(|e| LoaderError::Internal { key: key.to_string(), @@ -60,13 +57,17 @@ impl DecisionLoader { return Err(LoaderError::NotFound(key.to_string()).into()); }; - let decision_content = - serde_json::from_slice(buffer.as_ref()).map_err(|e| LoaderError::Internal { - key: key.to_string(), - source: e.into(), - })?; + let decision_content = match buffer { + Either::A(buf) => Arc::new(serde_json::from_slice(buf.as_ref()).map_err(|e| { + LoaderError::Internal { + key: key.to_string(), + source: e.into(), + } + })?), + Either::B(dc) => dc.inner.clone(), + }; - Ok(Arc::new(decision_content)) + Ok(decision_content) } } diff --git a/bindings/nodejs/test/decision.spec.ts b/bindings/nodejs/test/decision.spec.ts index 2bf9868f..76ad4ce8 100644 --- a/bindings/nodejs/test/decision.spec.ts +++ b/bindings/nodejs/test/decision.spec.ts @@ -4,7 +4,7 @@ import { evaluateUnaryExpression, renderTemplate, evaluateExpressionSync, - evaluateUnaryExpressionSync, renderTemplateSync + evaluateUnaryExpressionSync, renderTemplateSync, ZenDecisionContent } from "../index"; import fs from 'fs/promises'; import path from 'path'; @@ -81,6 +81,13 @@ describe('ZenEngine', () => { const r = await engine.evaluate('custom.json', {a: 5}); expect(r.result.data).toEqual(25); }); + + it('Parses ZenDecisionContent', async () => { + const decisionContent = new ZenDecisionContent(await loader('table.json')); + + expect(decisionContent).toBeDefined(); + expect(decisionContent.toBuffer()).toBeDefined(); + }); }) describe('Expressions', () => {