From a03978fbe7db82c97b362d5da726886f19d89bf6 Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:21:31 +0200 Subject: [PATCH] feat: add bigjs to function node; (#77) * feat: add bigjs to function node; * fix test * fix tests * fix fmt --- bindings/nodejs/src/engine.rs | 8 +++- bindings/nodejs/src/loader.rs | 9 ++-- bindings/python/src/engine.rs | 4 +- bindings/python/src/loader.rs | 2 +- core/engine/src/handler/function/script.rs | 4 +- .../src/handler/function/scripts/big.js | 7 ++++ .../src/handler/function/scripts/internals.js | 2 +- core/engine/src/handler/function/vm.rs | 4 ++ core/engine/tests/engine.rs | 42 ++++++++++++++++++- core/engine/tests/model.rs | 13 +++--- core/engine/tests/support/mod.rs | 9 ++-- core/expression/src/parser/iter.rs | 2 +- core/expression/src/vm.rs | 16 +++---- test-data/js/imports.js | 25 +++++++++++ 14 files changed, 118 insertions(+), 29 deletions(-) create mode 100644 core/engine/src/handler/function/scripts/big.js create mode 100644 test-data/js/imports.js diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index b0167122..8eecd98f 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -40,11 +40,15 @@ impl ZenEngine { #[napi(constructor)] pub fn new(mut env: Env, options: Option) -> napi::Result { let Some(opts) = options else { - return Ok(Self { graph: DecisionEngine::new(DecisionLoader::default()).into() }) + return Ok(Self { + graph: DecisionEngine::new(DecisionLoader::default()).into(), + }); }; let Some(loader_fn) = opts.loader else { - return Ok(Self { graph: DecisionEngine::new(DecisionLoader::default()).into() }) + return Ok(Self { + graph: DecisionEngine::new(DecisionLoader::default()).into(), + }); }; Ok(Self { diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index 619e2ab9..799d99d1 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -35,10 +35,11 @@ impl DecisionLoader { pub async fn get_key(&self, key: &str) -> LoaderResult> { let Some(function) = &self.function else { - return Err(LoaderError::Internal { - key: key.to_string(), - source: anyhow!("Loader is undefined") - }.into()) + return Err(LoaderError::Internal { + key: key.to_string(), + source: anyhow!("Loader is undefined"), + } + .into()); }; let promise: Promise> = function diff --git a/bindings/python/src/engine.rs b/bindings/python/src/engine.rs index a7c2c40c..41103563 100644 --- a/bindings/python/src/engine.rs +++ b/bindings/python/src/engine.rs @@ -44,11 +44,11 @@ impl PyZenEngine { #[new] pub fn new(maybe_options: Option<&PyDict>) -> PyResult { let Some(options) = maybe_options else { - return Ok(Default::default()) + return Ok(Default::default()); }; let Some(loader_any) = options.get_item("loader") else { - return Ok(Default::default()) + return Ok(Default::default()); }; let loader = Python::with_gil(|py| loader_any.to_object(py)); diff --git a/bindings/python/src/loader.rs b/bindings/python/src/loader.rs index 9f12243e..31f2b838 100644 --- a/bindings/python/src/loader.rs +++ b/bindings/python/src/loader.rs @@ -17,7 +17,7 @@ impl From for PyDecisionLoader { impl PyDecisionLoader { fn load_element(&self, key: &str) -> Result, anyhow::Error> { let Some(object) = &self.0 else { - return Err(anyhow!("Loader is not defined")) + return Err(anyhow!("Loader is not defined")); }; let content = Python::with_gil(|py| { diff --git a/core/engine/src/handler/function/script.rs b/core/engine/src/handler/function/script.rs index 7125b9c2..6eb8acaf 100644 --- a/core/engine/src/handler/function/script.rs +++ b/core/engine/src/handler/function/script.rs @@ -84,7 +84,9 @@ impl Script { return Err(anyhow!("Timeout exceeded")); } - let exception = tc_scope.exception().context("Failed to run loaded script")?; + let exception = tc_scope + .exception() + .context("Failed to run loaded script")?; return Err(anyhow!(exception.to_rust_string_lossy(tc_scope))); }; diff --git a/core/engine/src/handler/function/scripts/big.js b/core/engine/src/handler/function/scripts/big.js new file mode 100644 index 00000000..301bf0a0 --- /dev/null +++ b/core/engine/src/handler/function/scripts/big.js @@ -0,0 +1,7 @@ +/** + * Minified by jsDelivr using Terser v5.14.1. + * Original file: /npm/big.js@6.2.1/big.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(r){"use strict";var e,t=1e6,n=1e6,i="[big.js] ",o=i+"Invalid ",s=o+"decimal places",c=i+"Division by zero",f={},u=void 0,h=/^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;function l(r,e,t,n){var i=r.c;if(t===u&&(t=r.constructor.RM),0!==t&&1!==t&&2!==t&&3!==t)throw Error("[big.js] Invalid rounding mode");if(e<1)n=3===t&&(n||!!i[0])||0===e&&(1===t&&i[0]>=5||2===t&&(i[0]>5||5===i[0]&&(n||i[1]!==u))),i.length=1,n?(r.e=r.e-e+1,i[0]=1):i[0]=r.e=0;else if(e=5||2===t&&(i[e]>5||5===i[e]&&(n||i[e+1]!==u||1&i[e-1]))||3===t&&(n||!!i[0]),i.length=e,n)for(;++i[--e]>9;)if(i[e]=0,0===e){++r.e,i.unshift(1);break}for(e=i.length;!i[--e];)i.pop()}return r}function a(r,e,t){var n=r.e,i=r.c.join(""),o=i.length;if(e)i=i.charAt(0)+(o>1?"."+i.slice(1):"")+(n<0?"e":"e+")+n;else if(n<0){for(;++n;)i="0"+i;i="0."+i}else if(n>0)if(++n>o)for(n-=o;n--;)i+="0";else n1&&(i=i.charAt(0)+"."+i.slice(1));return r.s<0&&t?"-"+i:i}f.abs=function(){var r=new this.constructor(this);return r.s=1,r},f.cmp=function(r){var e,t=this,n=t.c,i=(r=new t.constructor(r)).c,o=t.s,s=r.s,c=t.e,f=r.e;if(!n[0]||!i[0])return n[0]?o:i[0]?-s:0;if(o!=s)return o;if(e=o<0,c!=f)return c>f^e?1:-1;for(s=(c=n.length)<(f=i.length)?c:f,o=-1;++oi[o]^e?1:-1;return c==f?0:c>f^e?1:-1},f.div=function(r){var e=this,n=e.constructor,i=e.c,o=(r=new n(r)).c,f=e.s==r.s?1:-1,h=n.DP;if(h!==~~h||h<0||h>t)throw Error(s);if(!o[0])throw Error(c);if(!i[0])return r.s=f,r.c=[r.e=0],r;var a,p,g,w,v,d=o.slice(),m=a=o.length,E=i.length,b=i.slice(0,a),P=b.length,D=r,M=D.c=[],x=0,y=h+(D.e=e.e-r.e)+1;for(D.s=f,f=y<0?0:y,d.unshift(0);P++P?1:-1;else for(v=-1,w=0;++vb[v]?1:-1;break}if(!(w<0))break;for(p=P==a?o:d;P;){if(b[--P]y&&l(D,y,n.RM,b[0]!==u),D},f.eq=function(r){return 0===this.cmp(r)},f.gt=function(r){return this.cmp(r)>0},f.gte=function(r){return this.cmp(r)>-1},f.lt=function(r){return this.cmp(r)<0},f.lte=function(r){return this.cmp(r)<1},f.minus=f.sub=function(r){var e,t,n,i,o=this,s=o.constructor,c=o.s,f=(r=new s(r)).s;if(c!=f)return r.s=-f,o.plus(r);var u=o.c.slice(),h=o.e,l=r.c,a=r.e;if(!u[0]||!l[0])return l[0]?r.s=-f:u[0]?r=new s(o):r.s=1,r;if(c=h-a){for((i=c<0)?(c=-c,n=u):(a=h,n=l),n.reverse(),f=c;f--;)n.push(0);n.reverse()}else for(t=((i=u.length0)for(;f--;)u[e++]=0;for(f=e;t>c;){if(u[--t]0?(f=s,n=u):(e=-e,n=c),n.reverse();e--;)n.push(0);n.reverse()}for(c.length-u.length<0&&(n=u,u=c,c=n),e=u.length,t=0;e;c[e]%=10)t=(c[--e]=c[e]+u[e]+t)/10|0;for(t&&(c.unshift(t),++f),e=c.length;0===c[--e];)c.pop();return r.c=c,r.e=f,r},f.pow=function(r){var e=this,t=new e.constructor("1"),i=t,s=r<0;if(r!==~~r||r<-1e6||r>n)throw Error(o+"exponent");for(s&&(r=-r);1&r&&(i=i.times(e)),r>>=1;)e=e.times(e);return s?t.div(i):i},f.prec=function(r,e){if(r!==~~r||r<1||r>t)throw Error(o+"precision");return l(new this.constructor(this),r,e)},f.round=function(r,e){if(r===u)r=0;else if(r!==~~r||r<-t||r>t)throw Error(s);return l(new this.constructor(this),r+this.e+1,e)},f.sqrt=function(){var r,e,t,n=this,o=n.constructor,s=n.s,c=n.e,f=new o("0.5");if(!n.c[0])return new o(n);if(s<0)throw Error(i+"No square root");0===(s=Math.sqrt(n+""))||s===1/0?((e=n.c.join("")).length+c&1||(e+="0"),c=((c+1)/2|0)-(c<0||1&c),r=new o(((s=Math.sqrt(e))==1/0?"5e":(s=s.toExponential()).slice(0,s.indexOf("e")+1))+c)):r=new o(s+""),c=r.e+(o.DP+=4);do{t=r,r=f.times(t.plus(n.div(t)))}while(t.c.slice(0,c).join("")!==r.c.slice(0,c).join(""));return l(r,(o.DP-=4)+r.e+1,o.RM)},f.times=f.mul=function(r){var e,t=this,n=t.constructor,i=t.c,o=(r=new n(r)).c,s=i.length,c=o.length,f=t.e,u=r.e;if(r.s=t.s==r.s?1:-1,!i[0]||!o[0])return r.c=[r.e=0],r;for(r.e=f+u,sf;)c=e[u]+o[f]*i[u-f-1]+c,e[u--]=c%10,c=c/10|0;e[u]=c}for(c?++r.e:e.shift(),f=e.length;!e[--f];)e.pop();return r.c=e,r},f.toExponential=function(r,e){var n=this,i=n.c[0];if(r!==u){if(r!==~~r||r<0||r>t)throw Error(s);for(n=l(new n.constructor(n),++r,e);n.c.lengtht)throw Error(s);for(r=r+(n=l(new n.constructor(n),r+n.e+1,e)).e+1;n.c.length=e.PE,!!r.c[0])},f.toNumber=function(){var r=Number(a(this,!0,!0));if(!0===this.constructor.strict&&!this.eq(r.toString()))throw Error(i+"Imprecise conversion");return r},f.toPrecision=function(r,e){var n=this,i=n.constructor,s=n.c[0];if(r!==u){if(r!==~~r||r<1||r>t)throw Error(o+"precision");for(n=l(new i(n),r,e);n.c.length=i.PE,!!s)},f.valueOf=function(){var r=this,e=r.constructor;if(!0===e.strict)throw Error(i+"valueOf disallowed");return a(r,r.e<=e.NE||r.e>=e.PE,!0)},e=function r(){function e(t){var n=this;if(!(n instanceof e))return t===u?r():new e(t);if(t instanceof e)n.s=t.s,n.e=t.e,n.c=t.c.slice();else{if("string"!=typeof t){if(!0===e.strict&&"bigint"!=typeof t)throw TypeError(o+"value");t=0===t&&1/t<0?"-0":String(t)}!function(r,e){var t,n,i;if(!h.test(e))throw Error(o+"number");r.s="-"==e.charAt(0)?(e=e.slice(1),-1):1,(t=e.indexOf("."))>-1&&(e=e.replace(".",""));(n=e.search(/e/i))>0?(t<0&&(t=n),t+=+e.slice(n+1),e=e.substring(0,n)):t<0&&(t=e.length);for(i=e.length,n=0;n0&&"0"==e.charAt(--i););for(r.e=t-n-1,r.c=[],t=0;n<=i;)r.c[t++]=+e.charAt(n++)}}(n,t)}n.constructor=e}return e.prototype=f,e.DP=20,e.RM=1,e.NE=-7,e.PE=21,e.strict=false,e.roundDown=0,e.roundHalfUp=1,e.roundHalfEven=2,e.roundUp=3,e}(),e.default=e.Big=e,"function"==typeof define&&define.amd?define((function(){return e})):"undefined"!=typeof module&&module.exports?module.exports=e:r.Big=e}(this); \ No newline at end of file diff --git a/core/engine/src/handler/function/scripts/internals.js b/core/engine/src/handler/function/scripts/internals.js index 60db49c7..127334d0 100644 --- a/core/engine/src/handler/function/scripts/internals.js +++ b/core/engine/src/handler/function/scripts/internals.js @@ -17,6 +17,6 @@ const console = { }; const main = (input) => ({ - output: handler(input, {moment: __GLOBAL__DAYJS, env: __GLOBAL__ENV}), + output: handler(input, {moment: __GLOBAL__DAYJS, dayjs: __GLOBAL__DAYJS, Big: Big, env: __GLOBAL__ENV}), log, }); diff --git a/core/engine/src/handler/function/vm.rs b/core/engine/src/handler/function/vm.rs index db303ce1..31dc579c 100644 --- a/core/engine/src/handler/function/vm.rs +++ b/core/engine/src/handler/function/vm.rs @@ -34,12 +34,16 @@ pub static BASE_VM: Lazy> = Lazy::new(|| { let scope = &mut v8::ContextScope::new(handle_scope, context); let dayjs_src = v8::String::new(scope, include_str!("scripts/dayjs.js")).unwrap(); + let bigjs_src = v8::String::new(scope, include_str!("scripts/big.js")).unwrap(); let internal_src = v8::String::new(scope, include_str!("scripts/internals.js")).unwrap(); let env_src = v8::String::new(scope, env_src_string.as_str()).unwrap(); v8::Script::compile(scope, dayjs_src, None) .unwrap() .run(scope); + v8::Script::compile(scope, bigjs_src, None) + .unwrap() + .run(scope); v8::Script::compile(scope, internal_src, None) .unwrap() .run(scope); diff --git a/core/engine/tests/engine.rs b/core/engine/tests/engine.rs index 0f57d0d2..b4225ca1 100644 --- a/core/engine/tests/engine.rs +++ b/core/engine/tests/engine.rs @@ -1,9 +1,13 @@ -use crate::support::{create_fs_loader, load_test_data}; +use crate::support::{create_fs_loader, load_raw_test_data, load_test_data}; +use serde::Deserialize; use serde_json::json; +use std::io::Read; use std::ops::Deref; +use std::path::Path; use std::sync::Arc; use zen_engine::loader::{LoaderError, MemoryLoader}; +use zen_engine::model::DecisionNodeKind; use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions}; mod support; @@ -138,3 +142,39 @@ async fn engine_with_trace() { let trace = table_opt.trace.unwrap(); assert_eq!(trace.len(), 3); // trace for each node } + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_function_imports() { + let mut function_content = load_test_data("function.json"); + + let imports_js_path = Path::new("js").join("imports.js"); + let mut replace_buffer = load_raw_test_data(imports_js_path.to_str().unwrap()); + let mut replace_data = String::new(); + replace_buffer.read_to_string(&mut replace_data).unwrap(); + + function_content.nodes.iter_mut().for_each(|node| { + if let DecisionNodeKind::FunctionNode { content } = &mut node.kind { + let _ = std::mem::replace(content, replace_data.clone()); + } + }); + + let decision = DecisionEngine::default().create_decision(function_content.into()); + let response = decision.evaluate(&json!({})).await.unwrap(); + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct GraphResult { + bigjs_tests: Vec, + bigjs_valid: bool, + dayjs_valid: bool, + moment_valid: bool, + } + + let result = serde_json::from_value::(response.result).unwrap(); + + assert!(result.bigjs_tests.iter().all(|v| *v)); + assert!(result.bigjs_valid); + assert!(result.dayjs_valid); + assert!(result.moment_valid); +} diff --git a/core/engine/tests/model.rs b/core/engine/tests/model.rs index 7ad9611d..4fe89de6 100644 --- a/core/engine/tests/model.rs +++ b/core/engine/tests/model.rs @@ -33,12 +33,15 @@ mod bincode_tests { #[cfg_attr(miri, ignore)] fn jdm_serde() { let root_dir = test_data_root(); - let files = fs::read_dir(Path::new(root_dir.as_str())).unwrap(); - for maybe_file in files { - let file = maybe_file.unwrap(); - let file_contents = fs::read_to_string(file.path()).unwrap(); - let serialized = serde_json::from_str::(&file_contents).unwrap(); + let dir_entries = fs::read_dir(Path::new(root_dir.as_str())).unwrap(); + for maybe_dir_entry in dir_entries { + let dir_entry = maybe_dir_entry.unwrap(); + let Ok(file_contents) = fs::read_to_string(dir_entry.path()) else { + // We expect some directories to be skipped + continue; + }; + let serialized = serde_json::from_str::(&file_contents).unwrap(); assert!(serde_json::to_string(&serialized).is_ok()); } } diff --git a/core/engine/tests/support/mod.rs b/core/engine/tests/support/mod.rs index f7f816bf..8e0ecb05 100644 --- a/core/engine/tests/support/mod.rs +++ b/core/engine/tests/support/mod.rs @@ -17,11 +17,14 @@ pub fn test_data_root() -> String { .to_string() } +pub fn load_raw_test_data(key: &str) -> BufReader { + let file = File::open(Path::new(&test_data_root()).join(key)).unwrap(); + BufReader::new(file) +} + #[allow(dead_code)] pub fn load_test_data(key: &str) -> DecisionContent { - let file = File::open(Path::new(&test_data_root()).join(key)).unwrap(); - let reader = BufReader::new(file); - serde_json::from_reader(reader).unwrap() + serde_json::from_reader(load_raw_test_data(key)).unwrap() } #[allow(dead_code)] diff --git a/core/expression/src/parser/iter.rs b/core/expression/src/parser/iter.rs index 781bba7c..fa969ce5 100644 --- a/core/expression/src/parser/iter.rs +++ b/core/expression/src/parser/iter.rs @@ -51,7 +51,7 @@ impl<'a, 'b> ParserIterator<'a, 'b> { pub fn set_position(&self, position: usize) -> ParserResult<()> { let Some(token) = self.tokens.get(position) else { - return Err(ParserError::TokenOutOfBounds) + return Err(ParserError::TokenOutOfBounds); }; self.position.set(position); diff --git a/core/expression/src/vm.rs b/core/expression/src/vm.rs index 67c894fa..2a66b7be 100644 --- a/core/expression/src/vm.rs +++ b/core/expression/src/vm.rs @@ -491,8 +491,8 @@ impl<'a> VM<'a> { let Array(arr) = self.pop()? else { return Err(OpcodeErr { opcode: "Median".into(), - message: "Unsupported type".into() - }) + message: "Unsupported type".into(), + }); }; let mut num_arr = arr @@ -542,8 +542,8 @@ impl<'a> VM<'a> { let Array(arr) = self.pop()? else { return Err(OpcodeErr { opcode: "Mode".into(), - message: "Unsupported type".into() - }) + message: "Unsupported type".into(), + }); }; let num_arr = arr @@ -914,7 +914,7 @@ impl<'a> VM<'a> { return Err(OpcodeErr { opcode: "Matches".into(), message: "Unsupported type".into(), - }) + }); }; let regex = Regex::new(b).map_err(|_| OpcodeErr { @@ -932,7 +932,7 @@ impl<'a> VM<'a> { return Err(OpcodeErr { opcode: "Matches".into(), message: "Unsupported type".into(), - }) + }); }; let regex = Regex::new(b).map_err(|_| OpcodeErr { @@ -1093,8 +1093,8 @@ impl<'a> VM<'a> { let Array(arr) = current else { return Err(OpcodeErr { opcode: "Flatten".into(), - message: "Unsupported type".into() - }) + message: "Unsupported type".into(), + }); }; let mut flat_arr = Vec::new(); diff --git a/test-data/js/imports.js b/test-data/js/imports.js new file mode 100644 index 00000000..803d23f3 --- /dev/null +++ b/test-data/js/imports.js @@ -0,0 +1,25 @@ +// Moment exists for reverse compatibility + +/** + * @param input + * @param {{ + * dayjs: import('dayjs') + * moment: import('dayjs') + * Big: import('big.js').BigConstructor + * }} helpers + */ +const handler = (input, { dayjs, Big, moment }) => { + const momentValid = typeof moment === 'function' && Object.keys(moment).includes('isDayjs'); + const dayjsValid = typeof dayjs === 'function' && Object.keys(moment).includes('isDayjs'); + const bigjsValid = typeof Big === 'function'; + + return { + momentValid, + dayjsValid, + bigjsValid, + bigjsTests: [ + Big(0.1).add(0.2).eq(0.3), + Big(123.12).mul(0.1).round(2).eq(12.31), + ] + }; +} \ No newline at end of file