mirror of
https://github.com/gorules/zen.git
synced 2026-10-08 16:02:30 +00:00
feat: custom node (#138)
* feat: custom node * add zen template, expose $nodes and $root * add custom handler to go and nodejs * add support for python, improve bindings, add error to zen template * fix: correct binding exports for nodejs and python * fix benchmark * improve rust api, trim template in zen templates * update cargo action * update expression version * compile action * fix action format
This commit is contained in:
@@ -10,12 +10,15 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
napi = { version = "2.14.4", features = ["serde-json", "error_anyhow", "tokio_rt"] }
|
||||
napi-derive = "2.14.6"
|
||||
napi = { version = "2.16", features = ["serde-json", "error_anyhow", "tokio_rt"] }
|
||||
napi-derive = "2.16"
|
||||
serde_json = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
zen-engine = { path = "../../core/engine" }
|
||||
zen-expression = { path = "../../core/expression" }
|
||||
zen-template = { path = "../../core/template" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
json_dotpath = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.1.0"
|
||||
napi-build = "2.1.2"
|
||||
Vendored
+37
-2
@@ -9,17 +9,52 @@ export interface ZenEvaluateOptions {
|
||||
}
|
||||
export interface ZenEngineOptions {
|
||||
loader?: (key: string) => Promise<Buffer>
|
||||
customHandler?: (request: ZenEngineHandlerRequest) => Promise<ZenEngineHandlerResponse>
|
||||
}
|
||||
export function evaluateExpressionSync(expression: string, context?: any | undefined | null): any
|
||||
export function evaluateUnaryExpressionSync(expression: string, context: any): boolean
|
||||
export function renderTemplateSync(template: string, context: any): any
|
||||
export function evaluateExpression(expression: string, context?: any | undefined | null): Promise<any>
|
||||
export function evaluateUnaryExpression(expression: string, context: any): Promise<boolean>
|
||||
export function renderTemplate(template: string, context: any): Promise<any>
|
||||
export interface ZenEngineTrace {
|
||||
id: string
|
||||
name: string
|
||||
input: any
|
||||
output: any
|
||||
performance?: string
|
||||
traceData?: any
|
||||
}
|
||||
export interface ZenEngineResponse {
|
||||
performance: string
|
||||
result: any
|
||||
trace?: Record<string, ZenEngineTrace>
|
||||
}
|
||||
export interface ZenEngineHandlerResponse {
|
||||
output: any
|
||||
traceData?: any
|
||||
}
|
||||
export interface DecisionNode {
|
||||
id: string
|
||||
name: string
|
||||
kind: string
|
||||
config: any
|
||||
}
|
||||
export class ZenDecision {
|
||||
constructor()
|
||||
evaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): Promise<any>
|
||||
evaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): Promise<ZenEngineResponse>
|
||||
validate(): void
|
||||
}
|
||||
export class ZenEngine {
|
||||
constructor(options?: ZenEngineOptions | undefined | null)
|
||||
evaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise<any>
|
||||
evaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise<ZenEngineResponse>
|
||||
createDecision(content: Buffer): ZenDecision
|
||||
getDecision(key: string): Promise<ZenDecision>
|
||||
}
|
||||
export class ZenEngineHandlerRequest {
|
||||
input: any
|
||||
node: DecisionNode
|
||||
constructor()
|
||||
getField(path: string): unknown
|
||||
getFieldRaw(path: string): unknown
|
||||
}
|
||||
|
||||
@@ -281,9 +281,14 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { ZenDecision, ZenEngine, evaluateExpression, evaluateUnaryExpression } = nativeBinding
|
||||
const { ZenDecision, ZenEngine, evaluateExpressionSync, evaluateUnaryExpressionSync, renderTemplateSync, evaluateExpression, evaluateUnaryExpression, renderTemplate, ZenEngineHandlerRequest } = nativeBinding
|
||||
|
||||
module.exports.ZenDecision = ZenDecision
|
||||
module.exports.ZenEngine = ZenEngine
|
||||
module.exports.evaluateExpressionSync = evaluateExpressionSync
|
||||
module.exports.evaluateUnaryExpressionSync = evaluateUnaryExpressionSync
|
||||
module.exports.renderTemplateSync = renderTemplateSync
|
||||
module.exports.evaluateExpression = evaluateExpression
|
||||
module.exports.evaluateUnaryExpression = evaluateUnaryExpression
|
||||
module.exports.renderTemplate = renderTemplate
|
||||
module.exports.ZenEngineHandlerRequest = ZenEngineHandlerRequest
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use napi::anyhow::anyhow;
|
||||
use napi::bindgen_prelude::Promise;
|
||||
use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction};
|
||||
use napi::{Env, JsFunction};
|
||||
|
||||
use zen_engine::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest};
|
||||
use zen_engine::handler::node::{NodeResponse, NodeResult};
|
||||
|
||||
use crate::types::{ZenEngineHandlerRequest, ZenEngineHandlerResponse};
|
||||
|
||||
pub(crate) struct CustomNode {
|
||||
function: Option<ThreadsafeFunction<ZenEngineHandlerRequest, ErrorStrategy::Fatal>>,
|
||||
}
|
||||
|
||||
impl Default for CustomNode {
|
||||
fn default() -> Self {
|
||||
Self { function: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomNode {
|
||||
pub fn try_new(env: &mut Env, function: JsFunction) -> napi::Result<Self> {
|
||||
let mut tsf = function.create_threadsafe_function(
|
||||
0,
|
||||
|cx: ThreadSafeCallContext<ZenEngineHandlerRequest>| Ok(vec![cx.value]),
|
||||
)?;
|
||||
|
||||
tsf.unref(env)?;
|
||||
|
||||
Ok(Self {
|
||||
function: Some(tsf),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomNodeAdapter for CustomNode {
|
||||
async fn handle(&self, request: CustomNodeRequest<'_>) -> NodeResult {
|
||||
let Some(function) = &self.function else {
|
||||
return Err(anyhow!("Custom function is undefined"));
|
||||
};
|
||||
|
||||
let node_data = crate::types::DecisionNode::from(request.node);
|
||||
|
||||
let promise: Promise<ZenEngineHandlerResponse> = function
|
||||
.clone()
|
||||
.call_async(ZenEngineHandlerRequest {
|
||||
input: request.input.clone(),
|
||||
node: node_data,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| anyhow!(err.reason))?;
|
||||
|
||||
let result = promise.await.map_err(|err| anyhow!(err.reason))?;
|
||||
|
||||
Ok(NodeResponse {
|
||||
output: result.output,
|
||||
trace_data: result.trace_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::custom_node::CustomNode;
|
||||
use crate::engine::ZenEvaluateOptions;
|
||||
use crate::loader::DecisionLoader;
|
||||
use crate::types::ZenEngineResponse;
|
||||
use napi::anyhow::anyhow;
|
||||
use napi::tokio;
|
||||
use napi_derive::napi;
|
||||
@@ -8,10 +10,10 @@ use std::sync::Arc;
|
||||
use zen_engine::{Decision, EvaluationOptions};
|
||||
|
||||
#[napi]
|
||||
pub struct ZenDecision(pub(crate) Arc<Decision<DecisionLoader>>);
|
||||
pub struct ZenDecision(pub(crate) Arc<Decision<DecisionLoader, CustomNode>>);
|
||||
|
||||
impl From<Decision<DecisionLoader>> for ZenDecision {
|
||||
fn from(value: Decision<DecisionLoader>) -> Self {
|
||||
impl From<Decision<DecisionLoader, CustomNode>> for ZenDecision {
|
||||
fn from(value: Decision<DecisionLoader, CustomNode>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
@@ -28,7 +30,7 @@ impl ZenDecision {
|
||||
&self,
|
||||
context: Value,
|
||||
opts: Option<ZenEvaluateOptions>,
|
||||
) -> napi::Result<Value> {
|
||||
) -> napi::Result<ZenEngineResponse> {
|
||||
let decision = self.0.clone();
|
||||
let result = tokio::spawn(async move {
|
||||
let options = opts.unwrap_or_default();
|
||||
@@ -46,7 +48,7 @@ impl ZenDecision {
|
||||
anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string()))
|
||||
})?;
|
||||
|
||||
Ok(serde_json::to_value(&result)?)
|
||||
Ok(ZenEngineResponse::from(result))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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};
|
||||
@@ -11,7 +13,7 @@ use zen_engine::{DecisionEngine, EvaluationOptions};
|
||||
|
||||
#[napi]
|
||||
pub struct ZenEngine {
|
||||
graph: Arc<DecisionEngine<DecisionLoader>>,
|
||||
graph: Arc<DecisionEngine<DecisionLoader, CustomNode>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
@@ -33,6 +35,9 @@ impl Default for ZenEvaluateOptions {
|
||||
pub struct ZenEngineOptions {
|
||||
#[napi(ts_type = "(key: string) => Promise<Buffer>")]
|
||||
pub loader: Option<JsFunction>,
|
||||
|
||||
#[napi(ts_type = "(request: ZenEngineHandlerRequest) => Promise<ZenEngineHandlerResponse>")]
|
||||
pub custom_handler: Option<JsFunction>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -41,18 +46,26 @@ impl ZenEngine {
|
||||
pub fn new(mut env: Env, options: Option<ZenEngineOptions>) -> napi::Result<Self> {
|
||||
let Some(opts) = options else {
|
||||
return Ok(Self {
|
||||
graph: DecisionEngine::new(DecisionLoader::default()).into(),
|
||||
graph: DecisionEngine::new(
|
||||
DecisionLoader::default().into(),
|
||||
CustomNode::default().into(),
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
};
|
||||
|
||||
let Some(loader_fn) = opts.loader else {
|
||||
return Ok(Self {
|
||||
graph: DecisionEngine::new(DecisionLoader::default()).into(),
|
||||
});
|
||||
let loader = match opts.loader {
|
||||
None => DecisionLoader::default(),
|
||||
Some(loader_fn) => DecisionLoader::try_new(&mut env, loader_fn)?,
|
||||
};
|
||||
|
||||
let custom_handler = match opts.custom_handler {
|
||||
None => CustomNode::default(),
|
||||
Some(custom_fn) => CustomNode::try_new(&mut env, custom_fn)?,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
graph: DecisionEngine::new(DecisionLoader::try_new(&mut env, loader_fn)?).into(),
|
||||
graph: DecisionEngine::new(loader.into(), custom_handler.into()).into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,7 +75,7 @@ impl ZenEngine {
|
||||
key: String,
|
||||
context: Value,
|
||||
opts: Option<ZenEvaluateOptions>,
|
||||
) -> napi::Result<Value> {
|
||||
) -> napi::Result<ZenEngineResponse> {
|
||||
let graph = self.graph.clone();
|
||||
let result = tokio::spawn(async move {
|
||||
let options = opts.unwrap_or_default();
|
||||
@@ -82,7 +95,7 @@ impl ZenEngine {
|
||||
anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string()))
|
||||
})?;
|
||||
|
||||
Ok(serde_json::to_value(&result)?)
|
||||
Ok(ZenEngineResponse::from(result))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
@@ -2,33 +2,55 @@ use napi::anyhow::anyhow;
|
||||
use napi_derive::napi;
|
||||
use serde_json::Value;
|
||||
|
||||
#[napi]
|
||||
pub fn evaluate_expression_sync(expression: String, context: Option<Value>) -> napi::Result<Value> {
|
||||
let ctx = context.unwrap_or(Value::Null);
|
||||
|
||||
Ok(
|
||||
zen_expression::evaluate_expression(expression.as_str(), &ctx)
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[napi]
|
||||
pub fn evaluate_unary_expression_sync(expression: String, context: Value) -> napi::Result<bool> {
|
||||
Ok(
|
||||
zen_expression::evaluate_unary_expression(expression.as_str(), &context)
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[napi]
|
||||
pub fn render_template_sync(template: String, context: Value) -> napi::Result<Value> {
|
||||
Ok(zen_template::render(template.as_str(), &context)
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[napi]
|
||||
pub async fn evaluate_expression(
|
||||
expression: String,
|
||||
context: Option<Value>,
|
||||
) -> napi::Result<Value> {
|
||||
let ctx = context.unwrap_or(Value::Null);
|
||||
|
||||
let result: Value = napi::tokio::spawn(async move {
|
||||
zen_expression::evaluate_expression(expression.as_str(), &ctx)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow!("Hook timed out"))?
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
|
||||
|
||||
Ok(result)
|
||||
napi::tokio::spawn(async move { evaluate_expression_sync(expression, context) })
|
||||
.await
|
||||
.map_err(|_| anyhow!("Hook timed out"))?
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[napi]
|
||||
pub async fn evaluate_unary_expression(expression: String, context: Value) -> napi::Result<bool> {
|
||||
let result: bool = napi::tokio::spawn(async move {
|
||||
zen_expression::evaluate_unary_expression(expression.as_str(), &context)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow!("Hook timed out"))?
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
|
||||
napi::tokio::spawn(async move { evaluate_unary_expression_sync(expression, context) })
|
||||
.await
|
||||
.map_err(|_| anyhow!("Hook timed out"))?
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
#[allow(dead_code)]
|
||||
#[napi]
|
||||
pub async fn render_template(template: String, context: Value) -> napi::Result<Value> {
|
||||
napi::tokio::spawn(async move { render_template_sync(template, context) })
|
||||
.await
|
||||
.map_err(|_| anyhow!("Hook timed out"))?
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod custom_node;
|
||||
mod decision;
|
||||
mod engine;
|
||||
mod expression;
|
||||
mod loader;
|
||||
mod types;
|
||||
|
||||
@@ -48,12 +48,12 @@ impl DecisionLoader {
|
||||
.await
|
||||
.map_err(|e| LoaderError::Internal {
|
||||
key: key.to_string(),
|
||||
source: e.into(),
|
||||
source: anyhow!(e.reason),
|
||||
})?;
|
||||
|
||||
let result = promise.await.map_err(|e| LoaderError::Internal {
|
||||
key: key.to_string(),
|
||||
source: e.into(),
|
||||
source: anyhow!(e.reason),
|
||||
})?;
|
||||
|
||||
let Some(buffer) = result else {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use json_dotpath::DotPaths;
|
||||
use napi::anyhow::{anyhow, Context};
|
||||
use napi_derive::napi;
|
||||
use serde_json::Value;
|
||||
|
||||
use zen_engine::handler::custom_node_adapter::CustomDecisionNode;
|
||||
use zen_engine::{DecisionGraphResponse, DecisionGraphTrace};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ZenEngineTrace {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub input: Value,
|
||||
pub output: Value,
|
||||
pub performance: Option<String>,
|
||||
pub trace_data: Option<Value>,
|
||||
}
|
||||
|
||||
impl From<DecisionGraphTrace> for ZenEngineTrace {
|
||||
fn from(value: DecisionGraphTrace) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
input: value.input,
|
||||
output: value.output,
|
||||
performance: value.performance,
|
||||
trace_data: value.trace_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ZenEngineResponse {
|
||||
pub performance: String,
|
||||
pub result: Value,
|
||||
pub trace: Option<HashMap<String, ZenEngineTrace>>,
|
||||
}
|
||||
|
||||
impl From<DecisionGraphResponse> for ZenEngineResponse {
|
||||
fn from(value: DecisionGraphResponse) -> Self {
|
||||
Self {
|
||||
performance: value.performance,
|
||||
result: value.result,
|
||||
trace: value.trace.map(|opt| {
|
||||
opt.into_iter()
|
||||
.map(|(key, value)| (key, ZenEngineTrace::from(value)))
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ZenEngineHandlerResponse {
|
||||
pub output: Value,
|
||||
pub trace_data: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[napi(object)]
|
||||
pub struct DecisionNode {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub config: Value,
|
||||
}
|
||||
|
||||
impl From<CustomDecisionNode<'_>> for DecisionNode {
|
||||
fn from(value: CustomDecisionNode<'_>) -> Self {
|
||||
Self {
|
||||
id: value.id.to_string(),
|
||||
name: value.name.to_string(),
|
||||
kind: value.kind.to_string(),
|
||||
config: value.config.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct ZenEngineHandlerRequest {
|
||||
pub input: Value,
|
||||
pub node: DecisionNode,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ZenEngineHandlerRequest {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> napi::Result<Self> {
|
||||
Err(anyhow!("Private constructor").into())
|
||||
}
|
||||
|
||||
#[napi(ts_return_type = "unknown")]
|
||||
pub fn get_field(&self, path: String) -> napi::Result<Value> {
|
||||
let node_config = &self.node.config;
|
||||
|
||||
let selected_value: Value = node_config
|
||||
.dot_get(path.as_str())
|
||||
.ok()
|
||||
.flatten()
|
||||
.context("Failed to find JSON path")?;
|
||||
let Value::String(template) = selected_value else {
|
||||
return Ok(selected_value);
|
||||
};
|
||||
|
||||
let template_value = zen_template::render(template.as_str(), &self.input)
|
||||
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
|
||||
|
||||
Ok(template_value)
|
||||
}
|
||||
|
||||
#[napi(ts_return_type = "unknown")]
|
||||
pub fn get_field_raw(&self, path: String) -> napi::Result<Value> {
|
||||
let node_config = &self.node.config;
|
||||
|
||||
let selected_value: Value = node_config
|
||||
.dot_get(path.as_str())
|
||||
.ok()
|
||||
.flatten()
|
||||
.context("Failed to find JSON path")?;
|
||||
|
||||
Ok(selected_value.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import {ZenEngine, evaluateExpression, evaluateUnaryExpression} from "../index";
|
||||
import {
|
||||
ZenEngine,
|
||||
evaluateExpression,
|
||||
evaluateUnaryExpression,
|
||||
renderTemplate,
|
||||
evaluateExpressionSync,
|
||||
evaluateUnaryExpressionSync, renderTemplateSync
|
||||
} from "../index";
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {describe, expect, it, jest} from "@jest/globals";
|
||||
@@ -49,6 +56,31 @@ describe('ZenEngine', () => {
|
||||
const r = await functionDecision.evaluate({input: 15});
|
||||
expect(r.result.output).toEqual(30);
|
||||
}, 10000)
|
||||
|
||||
it('Evaluate custom nodes with a handler', async () => {
|
||||
const engine = new ZenEngine({
|
||||
loader,
|
||||
customHandler: async (request) => {
|
||||
const prop1 = request.getField('prop1') as number;
|
||||
const prop1Raw = request.getFieldRaw('prop1');
|
||||
|
||||
expect(prop1).toEqual(15);
|
||||
expect(prop1Raw).toEqual('{{ a + 10 }}')
|
||||
expect(request.node).toMatchObject({
|
||||
id: '138b3b11-ff46-450f-9704-3f3c712067b2',
|
||||
name: 'customNode1',
|
||||
kind: 'sum',
|
||||
config: {
|
||||
prop1: '{{ a + 10 }}'
|
||||
}
|
||||
});
|
||||
return {output: {data: prop1 + 10}}
|
||||
}
|
||||
});
|
||||
|
||||
const r = await engine.evaluate('custom.json', {a: 5});
|
||||
expect(r.result.data).toEqual(25);
|
||||
});
|
||||
})
|
||||
|
||||
describe('Expressions', () => {
|
||||
@@ -63,6 +95,7 @@ describe('Expressions', () => {
|
||||
|
||||
for (const {expression, result, context} of expressions) {
|
||||
expect(await evaluateExpression(expression, context)).toEqual(result);
|
||||
expect(evaluateExpressionSync(expression, context)).toEqual(result);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,6 +109,21 @@ describe('Expressions', () => {
|
||||
|
||||
for (const {expression, result, context} of expressions) {
|
||||
expect(await evaluateUnaryExpression(expression, context)).toEqual(result);
|
||||
expect(evaluateUnaryExpressionSync(expression, context)).toEqual(result);
|
||||
}
|
||||
});
|
||||
|
||||
it('Renders templates', async () => {
|
||||
const templateCases = [
|
||||
{template: '{{ a + 10 }}', context: {a: 10}, result: 20},
|
||||
{template: '{{ a + 10 }}', context: {a: 15}, result: 25},
|
||||
{template: '{{ a + 10 }}', context: {a: 20}, result: 30},
|
||||
{template: '{{ a + 10 }}', context: {a: 25}, result: 35},
|
||||
];
|
||||
|
||||
for (const {template, context, result} of templateCases) {
|
||||
expect(await renderTemplate(template, context)).toEqual(result);
|
||||
expect(renderTemplateSync(template, context)).toEqual(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user