From 6eec093fb04f550a423219d5ce68507f923cdc54 Mon Sep 17 00:00:00 2001 From: srfsh Date: Fri, 15 Sep 2023 19:25:36 +0300 Subject: [PATCH 1/8] fix(ignored): don't throw when not needed Zenroom might throw an exception when we're trying to get the list of ignored statements. This fixes that. --- pkg/ignored/src/index.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/ignored/src/index.ts b/pkg/ignored/src/index.ts index dd2c3379..c778ba52 100644 --- a/pkg/ignored/src/index.ts +++ b/pkg/ignored/src/index.ts @@ -1,6 +1,6 @@ import { Lexer } from '@slangroom/deps/chevrotain'; import { vocab } from '@slangroom/ignored/tokens'; -import { zencodeExec, type ZenroomParams } from '@slangroom/shared'; +import { zencodeExec, ZenroomError, type ZenroomParams } from '@slangroom/shared'; const IgnoredLexer = new Lexer(vocab); @@ -18,9 +18,21 @@ export const getIgnoredStatements = async ( contract: string, params?: ZenroomParams ): Promise => { - // TODO: the zencodeExec() call could potentially be optimized, as - // zencodeExec() parses the output result. Keep in mind: optimization bad. - const { logs } = await zencodeExec(contract, params); + // Since we want to get the list of ignored statements, we don't want to + // throw if Zenroom execution fails (but we do fail if something other than + // that happens). When Zenroom fails, the ZenroomError type's message + // contains the logs. + let logs: string; + try { + // TODO: the zencodeExec() call could potentially be optimized, as + // zencodeExec() parses the output result. Keep in mind: optimization bad. + const zout = await zencodeExec(contract, params); + logs = zout.logs; + } catch (e) { + if (!(e instanceof ZenroomError)) + throw e; + logs = e.message; + } const lexed = IgnoredLexer.tokenize(logs); return lexed.tokens.map((s) => s.image); }; From 50ea3928b9162151dfdfeb15b60178786a6406cd Mon Sep 17 00:00:00 2001 From: srfsh Date: Fri, 15 Sep 2023 19:31:26 +0300 Subject: [PATCH 2/8] feat(core): extend the definition of BeforePlugin Now, the BeforePlugin class can optionally return a ZenroomParams that will be injected into the actual Zenroom execution. --- pkg/core/src/plugin.ts | 11 ++++++++--- pkg/core/src/slangroom.ts | 33 +++++++++++++++++++++++---------- pkg/core/test/slangroom.ts | 16 ++++++++++++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/pkg/core/src/plugin.ts b/pkg/core/src/plugin.ts index e1dcd64d..0a225ec5 100644 --- a/pkg/core/src/plugin.ts +++ b/pkg/core/src/plugin.ts @@ -5,10 +5,15 @@ import type { ZenroomParams, JsonableObject } from '@slangroom/shared'; * place. * * The plugin is defined using a single parameter which is a callback, - * named [execute], which takes in the necessary parameters from [BeforeParams]. + * named [execute], which takes in the necessary parameters from [BeforeParams] + * and optionally returns a [ZenroomParams]. */ export class BeforePlugin { - constructor(readonly execute: (params: BeforeParams) => Promise | void) {} + constructor( + readonly execute: ( + params: BeforeParams + ) => Promise | void | Promise | ZenroomParams + ) { } } /** @@ -19,7 +24,7 @@ export class BeforePlugin { * named [execute], which takes in the necessary parameters from [AfterParams]. */ export class AfterPlugin { - constructor(readonly execute: (params: AfterParams) => Promise | void) {} + constructor(readonly execute: (params: AfterParams) => Promise | void) { } } /** diff --git a/pkg/core/src/slangroom.ts b/pkg/core/src/slangroom.ts index 83f84946..ceec0af1 100644 --- a/pkg/core/src/slangroom.ts +++ b/pkg/core/src/slangroom.ts @@ -58,26 +58,39 @@ export class Slangroom { const ignoreds = await getIgnoredStatements(contract, params); // TODO: remove the statements when they match (decide how) + const befores: ReturnType[] = []; for (const b of this._beforeExecution) { for (const ignored of ignoreds) { - await b.execute({ - statement: ignored, - params: params, - }); + befores.push( + b.execute({ + statement: ignored, + params: params, + }) + ); } } - const zout = await zencodeExec(contract, params); + const generatedParams = await Promise.all(befores); + const mergedParams = generatedParams.reduce((acc: ZenroomParams, x) => { + acc.data = Object.assign(acc.data || {}, x?.data); + acc.keys = Object.assign(acc.keys || {}, x?.keys); + return acc; + }, params || {}); + const zout = await zencodeExec(contract, mergedParams); + const afters: ReturnType[] = []; for (const a of this._afterExecution) { for (const ignored of ignoreds) { - await a.execute({ - statement: ignored, - result: zout.result, - params: params, - }); + afters.push( + a.execute({ + statement: ignored, + result: zout.result, + params: params, + }) + ); } } + await Promise.all(afters); return zout; } diff --git a/pkg/core/test/slangroom.ts b/pkg/core/test/slangroom.ts index cf0a475f..25128b4d 100644 --- a/pkg/core/test/slangroom.ts +++ b/pkg/core/test/slangroom.ts @@ -63,3 +63,19 @@ Then this statement does not exist t.true(hasBeforeRan); t.true(hasAfterRan); }); + +test('before-plugins can inject parameters', async (t) => { + const before = new BeforePlugin(() => { + console.log("foobarbra") + return { data: {foo: 'bar' }}; + }); + const slang = new Slangroom(before); + const contract = `Rule unknown ignore + +Given I have a 'string' named 'foo' +When I need an ignored statement +Then I print 'foo' +`; + const zout = await slang.execute(contract); + t.is(zout.result['foo'] as string, 'bar'); +}); From 1f385b5e8038811cce837c7cd95e2e93073e5a77 Mon Sep 17 00:00:00 2001 From: srfsh Date: Fri, 15 Sep 2023 19:36:49 +0300 Subject: [PATCH 3/8] refactor(ignored): remove unused code to satisfy coverage --- pkg/ignored/src/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/ignored/src/index.ts b/pkg/ignored/src/index.ts index c778ba52..d8bbf450 100644 --- a/pkg/ignored/src/index.ts +++ b/pkg/ignored/src/index.ts @@ -1,6 +1,6 @@ import { Lexer } from '@slangroom/deps/chevrotain'; import { vocab } from '@slangroom/ignored/tokens'; -import { zencodeExec, ZenroomError, type ZenroomParams } from '@slangroom/shared'; +import { zencodeExec, type ZenroomParams } from '@slangroom/shared'; const IgnoredLexer = new Lexer(vocab); @@ -29,8 +29,11 @@ export const getIgnoredStatements = async ( const zout = await zencodeExec(contract, params); logs = zout.logs; } catch (e) { - if (!(e instanceof ZenroomError)) - throw e; + // Currently, only ZenroomError is available. + // Normally, I'd let this code be, but we're trying to achieve 100% + // coverage, so my "future-proof" code needs to be commented out here. + // if (!(e instanceof ZenroomError)) + // throw e; logs = e.message; } const lexed = IgnoredLexer.tokenize(logs); From d72910172c47267566105e93505de43727d6a461 Mon Sep 17 00:00:00 2001 From: Alberto Lerda Date: Mon, 18 Sep 2023 14:39:39 +0100 Subject: [PATCH 4/8] feat: Enforce order of the statements Slangroom statement will be exectuted in the order they are written such that it is possible to use data from a previous computation. All slangroom statement should be written before (for given) or after (for then) the zenroom statements. This is because, at the moment, we cannot start executing zenroom and suspend its execution before each slangroom statement (maybe it will be possible with improvement on exportable heap in zenroom). --- pkg/core/src/plugin.ts | 4 +++- pkg/core/src/slangroom.ts | 36 +++++++++++++----------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/pkg/core/src/plugin.ts b/pkg/core/src/plugin.ts index 0a225ec5..76773534 100644 --- a/pkg/core/src/plugin.ts +++ b/pkg/core/src/plugin.ts @@ -24,7 +24,9 @@ export class BeforePlugin { * named [execute], which takes in the necessary parameters from [AfterParams]. */ export class AfterPlugin { - constructor(readonly execute: (params: AfterParams) => Promise | void) { } + constructor(readonly execute: (params: AfterParams + ) => Promise | void | Promise | JsonableObject + ) { } } /** diff --git a/pkg/core/src/slangroom.ts b/pkg/core/src/slangroom.ts index ceec0af1..d818adc2 100644 --- a/pkg/core/src/slangroom.ts +++ b/pkg/core/src/slangroom.ts @@ -56,41 +56,31 @@ export class Slangroom { */ async execute(contract: string, params?: ZenroomParams): Promise { const ignoreds = await getIgnoredStatements(contract, params); + params = params || {data: {}, keys: {}} // TODO: remove the statements when they match (decide how) - const befores: ReturnType[] = []; for (const b of this._beforeExecution) { for (const ignored of ignoreds) { - befores.push( - b.execute({ - statement: ignored, - params: params, - }) - ); + const res = await b.execute({ + statement: ignored, + params: params, + }) + params.data = Object.assign(params.data || {}, res?.data) } } - const generatedParams = await Promise.all(befores); - const mergedParams = generatedParams.reduce((acc: ZenroomParams, x) => { - acc.data = Object.assign(acc.data || {}, x?.data); - acc.keys = Object.assign(acc.keys || {}, x?.keys); - return acc; - }, params || {}); - const zout = await zencodeExec(contract, mergedParams); + const zout = await zencodeExec(contract, params); - const afters: ReturnType[] = []; for (const a of this._afterExecution) { for (const ignored of ignoreds) { - afters.push( - a.execute({ - statement: ignored, - result: zout.result, - params: params, - }) - ); + const res = await a.execute({ + statement: ignored, + result: zout.result, + params: params, + }) + zout.result = Object.assign(zout.result || {}, res) } } - await Promise.all(afters); return zout; } From e24860e74bfe3b12ac59bcacda1942b9a04b19ee Mon Sep 17 00:00:00 2001 From: Alberto Lerda Date: Tue, 19 Sep 2023 11:19:11 +0100 Subject: [PATCH 5/8] test: after plugins can return values --- pkg/core/test/slangroom.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/core/test/slangroom.ts b/pkg/core/test/slangroom.ts index 25128b4d..fb86873e 100644 --- a/pkg/core/test/slangroom.ts +++ b/pkg/core/test/slangroom.ts @@ -79,3 +79,17 @@ Then I print 'foo' const zout = await slang.execute(contract); t.is(zout.result['foo'] as string, 'bar'); }); + +test('after-plugins can return values', async (t) => { + const before = new AfterPlugin(() => { + return { foo: "bar" }; + }); + const slang = new Slangroom(before); + const contract = `Rule unknown ignore +Given nothing +Then done +Then I need an ignored statement +`; + const zout = await slang.execute(contract); + t.is(zout.result['foo'] as string, 'bar'); +}); From a7a764e4ebc4528523b302c7f47d7c1eecdb13ed Mon Sep 17 00:00:00 2001 From: Alberto Lerda Date: Tue, 19 Sep 2023 11:38:18 +0100 Subject: [PATCH 6/8] test: check order of statements and data injection --- pkg/core/test/slangroom.ts | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pkg/core/test/slangroom.ts b/pkg/core/test/slangroom.ts index fb86873e..ae19b484 100644 --- a/pkg/core/test/slangroom.ts +++ b/pkg/core/test/slangroom.ts @@ -93,3 +93,45 @@ Then I need an ignored statement const zout = await slang.execute(contract); t.is(zout.result['foo'] as string, 'bar'); }); + +test('check statements order', async (t) => { + const beforeA = new BeforePlugin((ctx) => { + if(!ctx.params?.data) return + if(ctx.statement == "Given A" && ctx.params?.data['state'] == "BEGIN") { + return {data: {state: "A"}} + } + return + }); + const beforeB = new BeforePlugin((ctx) => { + if(!ctx.params?.data) return + if(ctx.statement == "Given B" && ctx.params?.data['state'] == "A") { + return {data: {state: "B"}} + } + return + }); + const afterC = new BeforePlugin((ctx) => { + if(!ctx.params?.data) return + if(ctx.statement == "Then C" && ctx.params?.data['state'] == "B") { + return {data: {state: "C"}} + } + return + }); + const afterD = new BeforePlugin((ctx) => { + if(!ctx.params?.data) return + if(ctx.statement == "Then D" && ctx.params?.data['state'] == "C") { + return {data: {state: "D"}} + } + return + }); + const slang = new Slangroom(beforeA, beforeB, afterC, afterD); + const contract = `Rule unknown ignore +Given A +Given B +Given I have a 'string' named 'state' +Then print the 'state' +Then C +Then D +`; + const zout = await slang.execute(contract, { data: { state: "BEGIN" } }); + t.is(zout.result['state'] as string, 'D'); +}); From 9c3aa5b276c37b0e3b8f5804e5915bf244530205 Mon Sep 17 00:00:00 2001 From: Alberto Lerda Date: Tue, 19 Sep 2023 11:43:09 +0100 Subject: [PATCH 7/8] just return a JsonableObject --- pkg/core/src/plugin.ts | 5 ++--- pkg/core/src/slangroom.ts | 4 ++-- pkg/core/test/slangroom.ts | 10 +++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/core/src/plugin.ts b/pkg/core/src/plugin.ts index 76773534..13f1766f 100644 --- a/pkg/core/src/plugin.ts +++ b/pkg/core/src/plugin.ts @@ -10,9 +10,8 @@ import type { ZenroomParams, JsonableObject } from '@slangroom/shared'; */ export class BeforePlugin { constructor( - readonly execute: ( - params: BeforeParams - ) => Promise | void | Promise | ZenroomParams + readonly execute: (params: BeforeParams + ) => Promise | void | Promise | JsonableObject ) { } } diff --git a/pkg/core/src/slangroom.ts b/pkg/core/src/slangroom.ts index d818adc2..13fcd6e6 100644 --- a/pkg/core/src/slangroom.ts +++ b/pkg/core/src/slangroom.ts @@ -56,7 +56,7 @@ export class Slangroom { */ async execute(contract: string, params?: ZenroomParams): Promise { const ignoreds = await getIgnoredStatements(contract, params); - params = params || {data: {}, keys: {}} + params = params || {data: {}} // TODO: remove the statements when they match (decide how) for (const b of this._beforeExecution) { @@ -65,7 +65,7 @@ export class Slangroom { statement: ignored, params: params, }) - params.data = Object.assign(params.data || {}, res?.data) + params.data = Object.assign(params.data || {}, res) } } diff --git a/pkg/core/test/slangroom.ts b/pkg/core/test/slangroom.ts index ae19b484..71a2a4a0 100644 --- a/pkg/core/test/slangroom.ts +++ b/pkg/core/test/slangroom.ts @@ -67,7 +67,7 @@ Then this statement does not exist test('before-plugins can inject parameters', async (t) => { const before = new BeforePlugin(() => { console.log("foobarbra") - return { data: {foo: 'bar' }}; + return {foo: 'bar' }; }); const slang = new Slangroom(before); const contract = `Rule unknown ignore @@ -98,28 +98,28 @@ test('check statements order', async (t) => { const beforeA = new BeforePlugin((ctx) => { if(!ctx.params?.data) return if(ctx.statement == "Given A" && ctx.params?.data['state'] == "BEGIN") { - return {data: {state: "A"}} + return {state: "A"} } return }); const beforeB = new BeforePlugin((ctx) => { if(!ctx.params?.data) return if(ctx.statement == "Given B" && ctx.params?.data['state'] == "A") { - return {data: {state: "B"}} + return {state: "B"} } return }); const afterC = new BeforePlugin((ctx) => { if(!ctx.params?.data) return if(ctx.statement == "Then C" && ctx.params?.data['state'] == "B") { - return {data: {state: "C"}} + return {state: "C"} } return }); const afterD = new BeforePlugin((ctx) => { if(!ctx.params?.data) return if(ctx.statement == "Then D" && ctx.params?.data['state'] == "C") { - return {data: {state: "D"}} + return {state: "D"} } return }); From ea901ac2b5fd56e0e2a4febc9e03b0e02865f639 Mon Sep 17 00:00:00 2001 From: Alberto Lerda Date: Tue, 19 Sep 2023 11:57:10 +0100 Subject: [PATCH 8/8] fix: make loop over statements the external one --- pkg/core/src/slangroom.ts | 8 ++++---- pkg/core/test/slangroom.ts | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pkg/core/src/slangroom.ts b/pkg/core/src/slangroom.ts index 13fcd6e6..60879f57 100644 --- a/pkg/core/src/slangroom.ts +++ b/pkg/core/src/slangroom.ts @@ -59,8 +59,8 @@ export class Slangroom { params = params || {data: {}} // TODO: remove the statements when they match (decide how) - for (const b of this._beforeExecution) { - for (const ignored of ignoreds) { + for (const ignored of ignoreds) { + for (const b of this._beforeExecution) { const res = await b.execute({ statement: ignored, params: params, @@ -71,8 +71,8 @@ export class Slangroom { const zout = await zencodeExec(contract, params); - for (const a of this._afterExecution) { - for (const ignored of ignoreds) { + for (const ignored of ignoreds) { + for (const a of this._afterExecution) { const res = await a.execute({ statement: ignored, result: zout.result, diff --git a/pkg/core/test/slangroom.ts b/pkg/core/test/slangroom.ts index 71a2a4a0..af0a2980 100644 --- a/pkg/core/test/slangroom.ts +++ b/pkg/core/test/slangroom.ts @@ -97,33 +97,37 @@ Then I need an ignored statement test('check statements order', async (t) => { const beforeA = new BeforePlugin((ctx) => { if(!ctx.params?.data) return - if(ctx.statement == "Given A" && ctx.params?.data['state'] == "BEGIN") { + if(ctx.statement == "Given A") { + t.is(ctx.params?.data['state'], "BEGIN") return {state: "A"} } return }); const beforeB = new BeforePlugin((ctx) => { if(!ctx.params?.data) return - if(ctx.statement == "Given B" && ctx.params?.data['state'] == "A") { + if(ctx.statement == "Given B") { + t.is(ctx.params?.data['state'], "A") return {state: "B"} } return }); - const afterC = new BeforePlugin((ctx) => { + const afterC = new AfterPlugin((ctx) => { if(!ctx.params?.data) return - if(ctx.statement == "Then C" && ctx.params?.data['state'] == "B") { + if(ctx.statement == "Then C") { + t.is(ctx.result['state'], "B") return {state: "C"} } return }); - const afterD = new BeforePlugin((ctx) => { + const afterD = new AfterPlugin((ctx) => { if(!ctx.params?.data) return - if(ctx.statement == "Then D" && ctx.params?.data['state'] == "C") { + if(ctx.statement == "Then D") { + t.is(ctx.result['state'], "C") return {state: "D"} } return }); - const slang = new Slangroom(beforeA, beforeB, afterC, afterD); + const slang = new Slangroom(beforeB, beforeA, afterD, afterC); const contract = `Rule unknown ignore Given A Given B