diff --git a/package-lock.json b/package-lock.json index 9f54932d..3aa9582e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", - "@grnsft/if-core": "^0.0.22", + "@grnsft/if-core": "^0.0.23", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", @@ -1186,9 +1186,9 @@ } }, "node_modules/@grnsft/if-core": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/@grnsft/if-core/-/if-core-0.0.22.tgz", - "integrity": "sha512-uzgYrQNh/aecouRdM5xcdCMC8Wu7xAWrGqJWqABopi/2CGs0xbvrQU0bqtGkSs1otAMnv5t7ynr6mpUyBxdQrw==", + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@grnsft/if-core/-/if-core-0.0.23.tgz", + "integrity": "sha512-lP+ViXjlhcTSosomLGOAO4PM8Ug5qtb5LEdOouUvg01PoVUJwLLf/MJgYxCegP8maAMCv1n4s1uPx15ffZqMXg==", "dependencies": { "typescript": "^5.1.6", "zod": "^3.23.8" diff --git a/package.json b/package.json index eed57c1a..8386c8f1 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", - "@grnsft/if-core": "^0.0.22", + "@grnsft/if-core": "^0.0.23", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", diff --git a/src/__tests__/if-run/builtins/coefficient.test.ts b/src/__tests__/if-run/builtins/coefficient.test.ts index 0aa638d5..1d6152b5 100644 --- a/src/__tests__/if-run/builtins/coefficient.test.ts +++ b/src/__tests__/if-run/builtins/coefficient.test.ts @@ -3,6 +3,7 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Coefficient} from '../../../if-run/builtins/coefficient'; import {STRINGS} from '../../../if-run/config'; +import {CoefficientConfig} from '@grnsft/if-core/types'; const {InputValidationError, ConfigError} = ERRORS; const {MISSING_CONFIG} = STRINGS; @@ -113,7 +114,78 @@ describe('builtins/coefficient: ', () => { expect(result).toStrictEqual(expectedResult); }); - it('throws an error when global config is not provided.', () => { + it('successfully executes when a parameter has an arithmetic expression.', () => { + expect.assertions(1); + const config = { + 'input-parameter': '=3*carbon', + coefficient: 3, + 'output-parameter': 'carbon-product', + }; + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + const coefficient = Coefficient(config, parametersMetadata, {}); + + const expectedResult = [ + { + duration: 3600, + carbon: 3, + 'carbon-product': 27, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = coefficient.execute([ + { + duration: 3600, + carbon: 3, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error when the `coefficient` has wrong arithmetic expression.', () => { + const config = { + 'input-parameter': 'carbon', + coefficient: 'mock-param', + 'output-parameter': 'carbon-product', + }; + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + const coefficient = Coefficient( + config as any as CoefficientConfig, + parametersMetadata, + {} + ); + + expect.assertions(2); + + try { + coefficient.execute([ + { + duration: 3600, + carbon: 'some-param', + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + '"coefficient" parameter is expected number, received string. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when config is not provided.', () => { const config = undefined; const coefficient = Coefficient(config!, parametersMetadata, {}); diff --git a/src/__tests__/if-run/builtins/copy-param.test.ts b/src/__tests__/if-run/builtins/copy-param.test.ts index fcbd3155..f7d0dea8 100644 --- a/src/__tests__/if-run/builtins/copy-param.test.ts +++ b/src/__tests__/if-run/builtins/copy-param.test.ts @@ -177,6 +177,36 @@ describe('builtins/copy: ', () => { expect(result).toStrictEqual(expectedResult); }); + + it('successfully executes when the `from` contains arithmetic expression', () => { + const config = { + 'keep-existing': false, + from: '=3*size', + to: 'if-size', + }; + const copy = Copy(config, parametersMetadata, {}); + + const inputs = [ + { + timestamp: '2024-07-05T13:45:48.398Z', + duration: 3600, + size: 0.05, + }, + ]; + + const expectedResult = [ + { + timestamp: '2024-07-05T13:45:48.398Z', + duration: 3600, + 'if-size': 0.15000000000000002, + }, + ]; + + expect.assertions(1); + const result = copy.execute(inputs); + + expect(result).toEqual(expectedResult); + }); }); }); }); diff --git a/src/__tests__/if-run/builtins/divide.test.ts b/src/__tests__/if-run/builtins/divide.test.ts index 10236e5c..f81561f9 100644 --- a/src/__tests__/if-run/builtins/divide.test.ts +++ b/src/__tests__/if-run/builtins/divide.test.ts @@ -107,7 +107,7 @@ describe('builtins/divide: ', () => { expect(result).toStrictEqual(expectedResult); }); - it('returns a result when `denominator` is provded in input.', async () => { + it('returns a result when `denominator` is provided in input.', async () => { expect.assertions(1); const config = { numerator: 'vcpus-allocated', @@ -137,6 +137,65 @@ describe('builtins/divide: ', () => { expect(response).toEqual(expectedResult); }); + it('successfully executes when a parameter contains arithmetic expression.', () => { + expect.assertions(1); + + const config = { + numerator: '=3*"vcpus-allocated"', + denominator: 'duration', + output: 'vcpus-allocated-per-second', + }; + + const divide = Divide(config, parametersMetadata, {}); + const input = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]; + const response = divide.execute(input); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + 'vcpus-allocated-per-second': 72 / 3600, + }, + ]; + + expect(response).toEqual(expectedResult); + }); + + it('throws an error the `numerator` parameter has wrong arithmetic expression.', () => { + const config = { + numerator: '3*"vcpus-allocated"', + denominator: 'duration', + output: 'vcpus-allocated-per-second', + }; + + const divide = Divide(config, parametersMetadata, {}); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]; + expect.assertions(2); + try { + divide.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `numerator` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); + it('throws an error on missing params in input.', async () => { const expectedMessage = '"vcpus-allocated" parameter is required. Error code: invalid_type.'; diff --git a/src/__tests__/if-run/builtins/exponent.test.ts b/src/__tests__/if-run/builtins/exponent.test.ts index 89df469e..d90a388c 100644 --- a/src/__tests__/if-run/builtins/exponent.test.ts +++ b/src/__tests__/if-run/builtins/exponent.test.ts @@ -1,8 +1,12 @@ +import {ExponentConfig} from '@grnsft/if-core/types'; import {ERRORS} from '@grnsft/if-core/utils'; +import {STRINGS} from '../../../if-run/config'; import {Exponent} from '../../../if-run/builtins/exponent'; -const {InputValidationError} = ERRORS; +const {InputValidationError, ConfigError} = ERRORS; + +const {MISSING_CONFIG} = STRINGS; describe('builtins/exponent: ', () => { describe('Exponent: ', () => { @@ -124,7 +128,7 @@ describe('builtins/exponent: ', () => { } catch (error) { expect(error).toStrictEqual( new InputValidationError( - '"input-parameter" parameter is required. Error code: invalid_type.' + '"energy/base" parameter is required. Error code: invalid_type.' ) ); } @@ -145,7 +149,7 @@ describe('builtins/exponent: ', () => { } catch (error) { expect(error).toStrictEqual( new InputValidationError( - '"input-parameter" parameter is expected number, received string. Error code: invalid_type.' + '"energy/base" parameter is expected number, received string. Error code: invalid_type.' ) ); } @@ -180,6 +184,96 @@ describe('builtins/exponent: ', () => { expect(response).toEqual(expectedResult); }); + + it('successfully executes when a parameter contains arithmetic expression.', () => { + const config = { + 'input-parameter': "=2*'energy/base'", + exponent: 3, + 'output-parameter': 'energy', + }; + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + + const exponent = Exponent(config, parametersMetadata, {}); + + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + 'energy/base': 4, + energy: 512, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = exponent.execute([ + { + duration: 3600, + 'energy/base': 4, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error when the `exponent` has wrong arithmetic expression.', () => { + const config = { + 'input-parameter': "=2*'energy/base'", + exponent: "3*'mock-param'", + 'output-parameter': 'energy', + }; + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + + const exponent = Exponent( + config as any as ExponentConfig, + parametersMetadata, + {} + ); + + expect.assertions(2); + + try { + exponent.execute([ + { + duration: 3600, + 'energy/base': 4, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `exponent` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); + }); + + it('throws an error on missing config.', async () => { + const config = undefined; + const divide = Exponent(config!, parametersMetadata, {}); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigError(MISSING_CONFIG)); + } }); }); }); diff --git a/src/__tests__/if-run/builtins/interpolation.test.ts b/src/__tests__/if-run/builtins/interpolation.test.ts index f2fb7a44..b3952903 100644 --- a/src/__tests__/if-run/builtins/interpolation.test.ts +++ b/src/__tests__/if-run/builtins/interpolation.test.ts @@ -207,6 +207,67 @@ describe('builtins/interpolation: ', () => { expect(plugin.execute(inputs)).toEqual(outputs); }); + it('successfully executes when the config parameter contains an arithmetic expression.', () => { + const config = { + method: Method.LINEAR, + x: [0, 10, 50, 100], + y: [0.12, 0.32, 0.75, 1.02], + 'input-parameter': "=2*'cpu/utilization'", + 'output-parameter': 'interpolation-result', + }; + const inputs = [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/utilization': 90, + }, + ]; + + const plugin = Interpolation(config, parametersMetadata, {}); + const outputs = [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/utilization': 90, + 'interpolation-result': 0, + }, + ]; + + expect.assertions(1); + expect(plugin.execute(inputs)).toEqual(outputs); + }); + + it('throws an error the config parameter contains wrong arithmetic expression.', () => { + const config = { + method: Method.LINEAR, + x: [0, 10, 50, 100], + y: [0.12, 0.32, 0.75, 1.02], + 'input-parameter': "2*'cpu/utilization'", + 'output-parameter': 'interpolation-result', + }; + const inputs = [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/utilization': 90, + }, + ]; + + const plugin = Interpolation(config, parametersMetadata, {}); + + expect.assertions(2); + try { + plugin.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `input-parameter` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); + it('throws an when the config is not provided.', () => { const config = undefined; const plugin = Interpolation(config!, parametersMetadata, {}); diff --git a/src/__tests__/if-run/builtins/multiply.test.ts b/src/__tests__/if-run/builtins/multiply.test.ts index 00979ea3..3d92b1d8 100644 --- a/src/__tests__/if-run/builtins/multiply.test.ts +++ b/src/__tests__/if-run/builtins/multiply.test.ts @@ -1,8 +1,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Multiply} from '../../../if-run/builtins/multiply'; +import {STRINGS} from '../../../if-run/config'; -const {InputValidationError} = ERRORS; +const {InputValidationError, ConfigError} = ERRORS; + +const {MISSING_CONFIG} = STRINGS; describe('builtins/multiply: ', () => { describe('Multiply: ', () => { @@ -173,6 +176,86 @@ describe('builtins/multiply: ', () => { expect(response).toEqual(expectedResult); }); + + it('successfully executes when the config output parameter contains arithmetic expression.', () => { + expect.assertions(1); + + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': '=2*energy', + }; + const multiply = Multiply(config, parametersMetadata, {}); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu/energy': 2, + 'network/energy': 2, + 'memory/energy': 2, + }, + ]; + const response = multiply.execute(inputs); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu/energy': 2, + 'network/energy': 2, + 'memory/energy': 2, + energy: 16, + }, + ]; + + expect(response).toEqual(expectedResult); + }); + + it('throws an error the config output parameter has wrong arithmetic expression.', () => { + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': '2*energy', + }; + + const multiply = Multiply(config, parametersMetadata, {}); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu/energy': 2, + 'network/energy': 2, + 'memory/energy': 2, + }, + ]; + expect.assertions(2); + try { + multiply.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `output-parameter` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); + + it('throws an error on missing config.', async () => { + const config = undefined; + const multiply = Multiply(config!, parametersMetadata, {}); + + expect.assertions(1); + + try { + await multiply.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigError(MISSING_CONFIG)); + } + }); }); }); }); diff --git a/src/__tests__/if-run/builtins/regex.test.ts b/src/__tests__/if-run/builtins/regex.test.ts index 49974d16..d509c891 100644 --- a/src/__tests__/if-run/builtins/regex.test.ts +++ b/src/__tests__/if-run/builtins/regex.test.ts @@ -53,7 +53,7 @@ describe('builtins/regex: ', () => { expect(result).toStrictEqual(expectedResult); }); - it('successfully applies regex strategy with multiple matches', async () => { + it('successfully applies regex strategy with multiple matches.', async () => { const config = { parameter: 'cloud/instance-type', match: '/(?<=_)[^_]+?(?=_|$)/g', diff --git a/src/__tests__/if-run/builtins/sci-embodied.test.ts b/src/__tests__/if-run/builtins/sci-embodied.test.ts index 9014a098..e79f55dc 100644 --- a/src/__tests__/if-run/builtins/sci-embodied.test.ts +++ b/src/__tests__/if-run/builtins/sci-embodied.test.ts @@ -152,6 +152,94 @@ describe('builtins/sci-embodied:', () => { expect(error).toBeInstanceOf(InputValidationError); } }); + + it('successfully executes when a parameter contains arithmetic expression.', () => { + const config = { + 'baseline-vcpus': 1, + 'baseline-memory': 16, + lifespan: 157680000, + 'baseline-emissions': 2000000, + 'vcpu-emissions-constant': 100000, + 'memory-emissions-constant': 1172, + 'ssd-emissions-constant': 50000, + 'hdd-emissions-constant': 1 * 100000, + 'gpu-emissions-constant': '= 2 * "mock-param"', + 'output-parameter': 'embodied-carbon', + }; + const sciEmbodied = SciEmbodied(config, parametersMetadata, {}); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + vCPUs: 2, + 'mock-param': 150000, + }, + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + vCPUs: 4, + 'mock-param': 100000, + }, + ]; + + const result = sciEmbodied.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + vCPUs: 2, + 'embodied-carbon': 47.945205479452056, + 'mock-param': 150000, + }, + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + vCPUs: 4, + 'embodied-carbon': 52.51141552511416, + 'mock-param': 100000, + }, + ]); + }); + + it('throws an error the `gpu-emissions-constant` parameter has wrong arithmetic expression.', () => { + const config = { + 'baseline-vcpus': 1, + 'baseline-memory': 16, + lifespan: 157680000, + 'baseline-emissions': 2000000, + 'vcpu-emissions-constant': 100000, + 'memory-emissions-constant': 1172, + 'ssd-emissions-constant': 50000, + 'hdd-emissions-constant': 1 * 100000, + 'gpu-emissions-constant': '2 * "mock-param"', + 'output-parameter': 'embodied-carbon', + }; + const sciEmbodied = SciEmbodied(config, parametersMetadata, {}); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + vCPUs: 2, + 'mock-param': 150000, + }, + ]; + + expect.assertions(2); + + try { + sciEmbodied.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `gpu-emissions-constant` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); }); }); }); diff --git a/src/__tests__/if-run/builtins/sci.test.ts b/src/__tests__/if-run/builtins/sci.test.ts index 0576b1e4..bd737849 100644 --- a/src/__tests__/if-run/builtins/sci.test.ts +++ b/src/__tests__/if-run/builtins/sci.test.ts @@ -2,7 +2,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Sci} from '../../../if-run/builtins/sci'; -const {MissingInputDataError} = ERRORS; +import {STRINGS} from '../../../if-run/config'; + +const {MissingInputDataError, ConfigError, InputValidationError} = ERRORS; + +const {MISSING_CONFIG} = STRINGS; describe('builtins/sci:', () => { describe('Sci: ', () => { @@ -220,5 +224,85 @@ describe('builtins/sci:', () => { expect(result).toStrictEqual([{...inputs[0], sci: inputs[0].carbon}]); }); + + it('throws an error on missing config.', () => { + const config = undefined; + const sci = Sci(config!, parametersMetadata, {}); + + expect.assertions(1); + + try { + sci.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigError(MISSING_CONFIG)); + } + }); + + it('successfully executes when a parameter contains arithmetic expression.', () => { + const config = {'functional-unit': '=10*users'}; + const sci = Sci(config, parametersMetadata, {}); + expect.assertions(1); + + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.02, + 'carbon-embodied': 5, + carbon: 5.02, + users: 100, + duration: 1, + }, + ]; + const result = sci.execute(inputs); + + expect.assertions(1); + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.02, + 'carbon-embodied': 5, + carbon: 5.02, + users: 100, + duration: 1, + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + sci: 0.0050199999999999995, + }, + ]); + }); + + it('throws an error the `functional-unit` parameter has wrong arithmetic expression.', () => { + const config = {'functional-unit': '10*users'}; + const sci = Sci(config, parametersMetadata, {}); + expect.assertions(1); + + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.02, + 'carbon-embodied': 5, + carbon: 5.02, + users: 100, + duration: 1, + }, + ]; + + expect.assertions(2); + + try { + sci.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `functional-unit` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); }); }); diff --git a/src/__tests__/if-run/builtins/subtract.test.ts b/src/__tests__/if-run/builtins/subtract.test.ts index 3343e2ab..8cdb7b0a 100644 --- a/src/__tests__/if-run/builtins/subtract.test.ts +++ b/src/__tests__/if-run/builtins/subtract.test.ts @@ -2,7 +2,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Subtract} from '../../../if-run/builtins/subtract'; -const {InputValidationError} = ERRORS; +import {STRINGS} from '../../../if-run/config'; + +const {InputValidationError, ConfigError} = ERRORS; + +const {MISSING_CONFIG} = STRINGS; describe('builtins/subtract: ', () => { describe('Subtract: ', () => { @@ -171,5 +175,87 @@ describe('builtins/subtract: ', () => { expect(response).toEqual(expectedResult); }); }); + + it('successfully executes when the config output parameter contains an arithmetic expression.', () => { + expect.assertions(1); + + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': "= 2 * 'energy/diff'", + }; + const subtract = Subtract(config, parametersMetadata, {}); + + const expectedResult = [ + { + duration: 3600, + 'cpu/energy': 4, + 'network/energy': 2, + 'memory/energy': 1, + 'energy/diff': 2, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = subtract.execute([ + { + duration: 3600, + 'cpu/energy': 4, + 'network/energy': 2, + 'memory/energy': 1, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error the config output parameter has wrong arithmetic expression.', () => { + expect.assertions(2); + + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': "=2 & 'energy/diff'", + }; + const subtract = Subtract(config, parametersMetadata, {}); + + const inputs = [ + { + duration: 3600, + 'cpu/energy': 4, + 'network/energy': 2, + 'memory/energy': 1, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + try { + subtract.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `output-parameter` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); + + it('throws an error on missing config.', async () => { + const config = undefined; + const subtract = Subtract(config!, parametersMetadata, {}); + + expect.assertions(1); + + try { + await subtract.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigError(MISSING_CONFIG)); + } + }); }); }); diff --git a/src/__tests__/if-run/builtins/sum.test.ts b/src/__tests__/if-run/builtins/sum.test.ts index 30701c23..1d0e928f 100644 --- a/src/__tests__/if-run/builtins/sum.test.ts +++ b/src/__tests__/if-run/builtins/sum.test.ts @@ -199,6 +199,69 @@ describe('builtins/sum: ', () => { expect(response).toEqual(expectedResult); }); + + it('successfully executes when the config output parameter contains an arithmetic expression.', () => { + expect.assertions(1); + + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': "=2*'energy'", + }; + + const sum = Sum(config, parametersMetadata, {}); + const expectedResult = [ + { + duration: 3600, + 'cpu/energy': 1, + 'network/energy': 1, + 'memory/energy': 1, + energy: 6, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = sum.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu/energy': 1, + 'network/energy': 1, + 'memory/energy': 1, + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error the config output parameter has wrong arithmetic expression.', () => { + expect.assertions(2); + + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': "2*'energy'", + }; + + const sum = Sum(config, parametersMetadata, {}); + + try { + sum.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu/energy': 1, + 'network/energy': 1, + 'memory/energy': 1, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `output-parameter` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); }); }); }); diff --git a/src/__tests__/if-run/builtins/time-converter.test.ts b/src/__tests__/if-run/builtins/time-converter.test.ts index 646beb3b..90664c64 100644 --- a/src/__tests__/if-run/builtins/time-converter.test.ts +++ b/src/__tests__/if-run/builtins/time-converter.test.ts @@ -181,6 +181,66 @@ describe('builtins/time-converter: ', () => { expect(response).toEqual(expectedResult); }); + + it('successfully executes when the config output parameter contains an arithmetic expression.', () => { + expect.assertions(1); + + const config = { + 'input-parameter': '=2 * "energy-per-year"', + 'original-time-unit': 'year', + 'new-time-unit': 'duration', + 'output-parameter': 'energy-per-duration', + }; + + const timeConverter = TimeConverter(config, parametersMetadata, {}); + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-per-year': 10000, + 'energy-per-duration': 2.281589, + }, + ]; + + const result = timeConverter.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-per-year': 10000, + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error the config input parameter has wrong arithmetic expression.', () => { + expect.assertions(2); + const config = { + 'input-parameter': '2*"energy-per-year"', + 'original-time-unit': 'year', + 'new-time-unit': 'duration', + 'output-parameter': 'energy-per-duration', + }; + + const timeConverter = TimeConverter(config, parametersMetadata, {}); + + try { + timeConverter.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-per-year': 10000, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error).toEqual( + new InputValidationError( + 'The `input-parameter` contains an invalid arithmetic expression. It should start with `=` and include the symbols `*`, `+`, `-` and `/`.' + ) + ); + } + }); }); }); }); diff --git a/src/__tests__/if-run/builtins/time-sync.test.ts b/src/__tests__/if-run/builtins/time-sync.test.ts index 41e47548..17b1cf99 100644 --- a/src/__tests__/if-run/builtins/time-sync.test.ts +++ b/src/__tests__/if-run/builtins/time-sync.test.ts @@ -897,6 +897,51 @@ describe('builtins/time-sync:', () => { ); expect(DateTime.fromISO(result[0].timestamp).offset === 0); }); + + it('successfully executes when the `duration` contains an arithmetic expression.', () => { + expect.assertions(1); + + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:10.000Z', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + const result = timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3 * 2, + 'resources-total': 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + 'resources-total': null, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + 'resources-total': null, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 1, + 'resources-total': null, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); }); }); }); diff --git a/src/if-run/builtins/coefficient/index.ts b/src/if-run/builtins/coefficient/index.ts index 0a002f67..12de779d 100644 --- a/src/if-run/builtins/coefficient/index.ts +++ b/src/if-run/builtins/coefficient/index.ts @@ -1,5 +1,13 @@ -import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import {z, ZodType} from 'zod'; + +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + getParameterFromArithmeticExpression, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -34,17 +42,30 @@ export const Coefficient = ( * Calculate the product of each input parameter. */ const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const inputParameter = safeGlobalConfig['input-parameter']; - const outputParameter = safeGlobalConfig['output-parameter']; - const coefficient = safeGlobalConfig['coefficient']; - + const safeConfig = validateConfig(); + const { + 'input-parameter': inputParameter, + 'output-parameter': outputParameter, + } = safeConfig; return inputs.map(input => { - validateSingleInput(input, inputParameter); + const calculatedConfig = evaluateConfig({ + config: safeConfig, + input, + parametersToEvaluate: ['input-parameter', 'coefficient'], + }); + + const safeInput = validateSingleInput(input, inputParameter); + const coefficient = Number(calculatedConfig['coefficient']); + const calculatedResult = calculateProduct( + safeInput, + calculatedConfig['input-parameter'], + coefficient + ); const result = { ...input, - [outputParameter]: calculateProduct(input, inputParameter, coefficient), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); @@ -54,14 +75,21 @@ export const Coefficient = ( /** * Checks for required fields in input. */ - const validateSingleInput = (input: PluginParams, inputParameter: string) => { + const validateSingleInput = ( + input: PluginParams, + configInputParameter: string + ) => { + const inputParameter = + getParameterFromArithmeticExpression(configInputParameter); + const evaluatedInput = evaluateInput(input); + const inputData = { - 'input-parameter': input[inputParameter], + [inputParameter]: evaluatedInput[inputParameter], }; const validationSchema = z.record(z.string(), z.number()); validate(validationSchema, inputData); - return input; + return evaluatedInput; }; /** @@ -69,9 +97,11 @@ export const Coefficient = ( */ const calculateProduct = ( input: PluginParams, - inputParameter: string, + inputParameter: string | number, coefficient: number - ) => input[inputParameter] * coefficient; + ) => + (isNaN(Number(inputParameter)) ? input[inputParameter] : inputParameter) * + coefficient; /** * Checks config value are valid. @@ -83,13 +113,27 @@ export const Coefficient = ( const mappedConfig = mapConfigIfNeeded(config, mapping); - const configSchema = z.object({ - coefficient: z.number(), - 'input-parameter': z.string().min(1), - 'output-parameter': z.string().min(1), - }); - - return validate>(configSchema, mappedConfig); + const configSchema = z + .object({ + coefficient: z.preprocess( + value => validateArithmeticExpression('coefficient', value), + z.number() + ), + 'input-parameter': z.string().min(1), + 'output-parameter': z.string().min(1), + }) + .refine(params => { + Object.entries(params).forEach(([param, value]) => + validateArithmeticExpression(param, value) + ); + + return true; + }); + + return validate>( + configSchema as ZodType, + mappedConfig + ); }; return { diff --git a/src/if-run/builtins/copy-param/index.ts b/src/if-run/builtins/copy-param/index.ts index fec1fd85..d10fb554 100644 --- a/src/if-run/builtins/copy-param/index.ts +++ b/src/if-run/builtins/copy-param/index.ts @@ -1,5 +1,11 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + getParameterFromArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -60,43 +66,54 @@ export const Copy = ( */ const validateSingleInput = ( input: PluginParams, - inputParameters: (string | number)[] + configInputParameter: string | number ) => { - const inputData = inputParameters.reduce( - (acc, param) => { - acc[param] = input[param]; - - return acc; - }, - {} as Record + const inputParameter = getParameterFromArithmeticExpression( + configInputParameter.toString() ); + const evaluatedInput = evaluateInput(input); + const inputData = { + [inputParameter]: evaluatedInput[inputParameter], + }; const validationSchema = z.record(z.string(), z.string().or(z.number())); validate(validationSchema, inputData); - return input; + return evaluatedInput; }; const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const keepExisting = safeGlobalConfig['keep-existing'] === true; - const from = safeGlobalConfig['from']; - const to = safeGlobalConfig['to']; + const safeConfig = validateConfig(); + const keepExisting = safeConfig['keep-existing'] === true; + const from = safeConfig['from']; + const to = safeConfig['to']; return inputs.map(input => { - const safeInput = validateSingleInput(input, [from]); + const evaluatedConfig = evaluateConfig({ + config: safeConfig, + input, + parametersToEvaluate: ['from'], + }); + + const safeInput = validateSingleInput(input, from); + const safeFrom = getParameterFromArithmeticExpression(from.toString()); + + const outputValue = !isNaN(evaluatedConfig?.from) + ? evaluatedConfig.from + : safeInput[safeFrom]; - const outputValue = safeInput[from]; - if (safeInput[from]) { + if (safeInput[safeFrom]) { if (!keepExisting) { - delete safeInput[from]; + delete input[safeFrom]; + delete safeInput[safeFrom]; } } const result = { - ...safeInput, // need to return or what you provide won't be outputted, don't be evil! - [to]: outputValue, + ...input, + ...safeInput, + ...evaluateArithmeticOutput(to, outputValue), }; return mapOutputIfNeeded(result, mapping); diff --git a/src/if-run/builtins/csv-lookup/index.ts b/src/if-run/builtins/csv-lookup/index.ts index f66981b9..63a4a349 100644 --- a/src/if-run/builtins/csv-lookup/index.ts +++ b/src/if-run/builtins/csv-lookup/index.ts @@ -203,8 +203,8 @@ export const CSVLookup = ( * 4. Filters requested information from CSV. */ const execute = async (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const {filepath, query, output} = safeGlobalConfig; + const safeConfig = validateConfig(); + const {filepath, query, output} = safeConfig; const file = await retrieveFile(filepath); const parsedCSV = parseCSVFile(file); diff --git a/src/if-run/builtins/divide/index.ts b/src/if-run/builtins/divide/index.ts index 767fd1d6..07ab3ba8 100644 --- a/src/if-run/builtins/divide/index.ts +++ b/src/if-run/builtins/divide/index.ts @@ -1,5 +1,12 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, + getParameterFromArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -34,19 +41,25 @@ export const Divide = ( * Calculate the division of each input parameter. */ const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const {numerator, denominator, output} = safeGlobalConfig; + const safeConfig = validateConfig(); + const {numerator, denominator, output} = safeConfig; return inputs.map((input, index) => { - const safeInput = Object.assign( - {}, + const evaluatedConfig = evaluateConfig({ + config: safeConfig, input, - validateSingleInput(input, {numerator, denominator}) - ); + parametersToEvaluate: ['numerator', 'denominator'], + }); + const safeInput = validateSingleInput(input, safeConfig); + const calculatedResult = calculateDivide(safeInput, index, { + numerator: evaluatedConfig.numerator || numerator, + denominator: evaluatedConfig.denominator || denominator, + }); const result = { ...input, - [output]: calculateDivide(safeInput, index, {numerator, denominator}), + ...safeInput, + ...evaluateArithmeticOutput(output, calculatedResult), }; return mapOutputIfNeeded(result, mapping); @@ -62,11 +75,19 @@ export const Divide = ( } const mappedConfig = mapConfigIfNeeded(config, mapping); - const schema = z.object({ - numerator: z.string().min(1), - denominator: z.string().or(z.number()), - output: z.string(), - }); + const schema = z + .object({ + numerator: z.string().min(1), + denominator: z.string().or(z.number()), + output: z.string(), + }) + .refine(params => { + Object.entries(params).forEach(([param, value]) => + validateArithmeticExpression(param, value) + ); + + return true; + }); return validate>(schema, mappedConfig); }; @@ -76,12 +97,14 @@ export const Divide = ( */ const validateSingleInput = ( input: PluginParams, - params: { - numerator: string; - denominator: number | string; - } + safeConfig: ConfigParams ) => { - const {numerator, denominator} = params; + const numerator = getParameterFromArithmeticExpression( + safeConfig.numerator + ); + const denominator = getParameterFromArithmeticExpression( + safeConfig.denominator + ); const schema = z .object({ @@ -96,7 +119,8 @@ export const Divide = ( return true; }); - return validate>(schema, input); + const evaluatedInput = evaluateInput(input); + return validate>(schema, evaluatedInput); }; /** @@ -106,19 +130,24 @@ export const Divide = ( input: PluginParams, index: number, params: { - numerator: string; + numerator: number | string; denominator: number | string; } ) => { const {denominator, numerator} = params; - const finalDenominator = input[denominator] || denominator; + const finalDenominator = + typeof denominator === 'number' + ? denominator + : input[denominator] || denominator; + const finalNumerator = + typeof numerator === 'number' ? numerator : input[numerator]; if (finalDenominator === 0) { console.warn(ZERO_DIVISION(Divide.name, index)); - return input[numerator]; + return finalNumerator; } - return input[numerator] / finalDenominator; + return finalNumerator / finalDenominator; }; return { diff --git a/src/if-run/builtins/exponent/index.ts b/src/if-run/builtins/exponent/index.ts index 199f44b1..500ea96a 100644 --- a/src/if-run/builtins/exponent/index.ts +++ b/src/if-run/builtins/exponent/index.ts @@ -1,8 +1,16 @@ -import {z} from 'zod'; +import {z, ZodType} from 'zod'; import { mapConfigIfNeeded, mapOutputIfNeeded, } from '@grnsft/if-core/utils/helpers'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + getParameterFromArithmeticExpression, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { ExecutePlugin, PluginParams, @@ -10,7 +18,6 @@ import { PluginParametersMetadata, MappingParams, } from '@grnsft/if-core/types'; -import {ERRORS} from '@grnsft/if-core/utils'; import {validate} from '../../../common/util/validations'; @@ -39,44 +46,82 @@ export const Exponent = ( } const mappedConfig = mapConfigIfNeeded(config, mapping); - const configSchema = z.object({ - 'input-parameter': z.string().min(1), - exponent: z.number(), - 'output-parameter': z.string().min(1), - }); - - return validate>(configSchema, mappedConfig); + const configSchema = z + .object({ + 'input-parameter': z.string().min(1), + exponent: z.preprocess( + value => validateArithmeticExpression('exponent', value), + z.number() + ), + 'output-parameter': z.string().min(1), + }) + .refine(params => { + Object.entries(params).forEach(([param, value]) => + validateArithmeticExpression(param, value) + ); + + return true; + }); + + return validate>( + configSchema as ZodType, + mappedConfig + ); }; /** * Checks for required fields in input. */ - const validateSingleInput = (input: PluginParams, inputParameter: string) => { + const validateSingleInput = ( + input: PluginParams, + configInputParameter: string | number + ) => { + const inputParameter = + typeof configInputParameter === 'number' + ? configInputParameter + : getParameterFromArithmeticExpression(configInputParameter); + const evaluatedInput = evaluateInput(input); + const inputData = { - 'input-parameter': input[inputParameter], + [inputParameter]: + typeof inputParameter === 'number' + ? inputParameter + : evaluatedInput[inputParameter], }; const validationSchema = z.record(z.string(), z.number()); - validate(validationSchema, inputData); - return input; + return validate(validationSchema, inputData); }; /** * Calculate the input param raised by to the power of the given exponent. */ const execute = (inputs: PluginParams[]): PluginParams[] => { + const safeConfig = validateConfig(); const { 'input-parameter': inputParameter, exponent, 'output-parameter': outputParameter, - } = validateConfig(); + } = safeConfig; return inputs.map(input => { - validateSingleInput(input, inputParameter); + const safeInput = validateSingleInput(input, inputParameter); + const evaluatedConfig = evaluateConfig({ + config: safeConfig, + input, + parametersToEvaluate: ['input-parameter', 'exponent'], + }); + + const calculatedResult = calculateExponent( + safeInput, + evaluatedConfig['input-parameter'] || inputParameter, + evaluatedConfig.exponent || exponent + ); const result = { ...input, - [outputParameter]: calculateExponent(input, inputParameter, exponent), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); @@ -88,10 +133,13 @@ export const Exponent = ( */ const calculateExponent = ( input: PluginParams, - inputParameter: string, + inputParameter: string | number, exponent: number ) => { - const base = input[inputParameter]; + const base = + typeof inputParameter === 'number' + ? inputParameter + : input[inputParameter]; return Math.pow(base, exponent); }; diff --git a/src/if-run/builtins/interpolation/index.ts b/src/if-run/builtins/interpolation/index.ts index c7c9887f..468c9d98 100644 --- a/src/if-run/builtins/interpolation/index.ts +++ b/src/if-run/builtins/interpolation/index.ts @@ -1,6 +1,13 @@ import Spline from 'typescript-cubic-spline'; import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, + getParameterFromArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -38,16 +45,22 @@ export const Interpolation = ( */ const execute = (inputs: PluginParams[]) => { const validatedConfig = validateConfig(); + const {'output-parameter': outputParameter} = validatedConfig; return inputs.map((input, index) => { + const calculatedConfig = evaluateConfig({ + config: validatedConfig, + input, + parametersToEvaluate: ['input-parameter'], + }); const safeInput = validateInput(input, index); + const calculatedResult = calculateResult(calculatedConfig, safeInput); + const result = { ...input, - [validatedConfig['output-parameter']]: calculateResult( - validatedConfig, - safeInput - ), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); @@ -74,7 +87,10 @@ export const Interpolation = ( config: ConfigParams, input: PluginParams ) => { - const parameter = input[config['input-parameter']]; + const parameter = + typeof config['input-parameter'] === 'number' + ? config['input-parameter'] + : input[config['input-parameter']]; const xPoints: number[] = config.x; const yPoints: number[] = config.y; @@ -104,7 +120,10 @@ export const Interpolation = ( config: ConfigParams, input: PluginParams ) => { - const parameter = input[config['input-parameter']]; + const parameter = + typeof config['input-parameter'] === 'number' + ? config['input-parameter'] + : input[config['input-parameter']]; const xPoints: number[] = config.x; const yPoints: number[] = config.y; const spline: any = new Spline(xPoints, yPoints); @@ -119,7 +138,10 @@ export const Interpolation = ( config: ConfigParams, input: PluginParams ) => { - const parameter = input[config['input-parameter']]; + const parameter = + typeof config['input-parameter'] === 'number' + ? config['input-parameter'] + : input[config['input-parameter']]; const xPoints: number[] = config.x; const yPoints: number[] = config.y; @@ -154,7 +176,11 @@ export const Interpolation = ( method: z.nativeEnum(Method), x: z.array(z.number()), y: z.array(z.number()), - 'input-parameter': z.string(), + 'input-parameter': z + .string() + .refine(param => + validateArithmeticExpression('input-parameter', param) + ), 'output-parameter': z.string(), }) .refine(data => data.x && data.y && data.x.length === data.y.length, { @@ -182,7 +208,10 @@ export const Interpolation = ( * Validates inputes parameters. */ const validateInput = (input: PluginParams, index: number) => { - const inputParameter = config['input-parameter']; + const inputParameter = getParameterFromArithmeticExpression( + config['input-parameter'] + ); + const schema = z .object({ timestamp: z.string().or(z.date()), @@ -198,7 +227,8 @@ export const Interpolation = ( } ); - return validate>(schema, input, index); + const evaluatedInput = evaluateInput(input); + return validate>(schema, evaluatedInput, index); }; return { diff --git a/src/if-run/builtins/multiply/index.ts b/src/if-run/builtins/multiply/index.ts index 530cb77a..a701f9b4 100644 --- a/src/if-run/builtins/multiply/index.ts +++ b/src/if-run/builtins/multiply/index.ts @@ -1,5 +1,10 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateArithmeticOutput, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -42,7 +47,12 @@ export const Multiply = ( const configSchema = z.object({ 'input-parameters': z.array(z.string()), - 'output-parameter': z.string().min(1), + 'output-parameter': z + .string() + .min(1) + .refine(param => + validateArithmeticExpression('output-parameter', param) + ), }); return validate>(configSchema, mappedConfig); @@ -55,9 +65,11 @@ export const Multiply = ( input: PluginParams, inputParameters: string[] ) => { + const evaluatedInput = evaluateInput(input); + const inputData = inputParameters.reduce( (acc, param) => { - acc[param] = input[param]; + acc[param] = evaluatedInput[param]; return acc; }, @@ -66,25 +78,27 @@ export const Multiply = ( const validationSchema = z.record(z.string(), z.number()); - validate(validationSchema, inputData); - - return input; + return validate(validationSchema, inputData); }; /** * Calculate the product of each input parameter. */ const execute = (inputs: PluginParams[]): PluginParams[] => { - const safeGlobalConfig = validateConfig(); - const inputParameters = safeGlobalConfig['input-parameters']; - const outputParameter = safeGlobalConfig['output-parameter']; + const safeConfig = validateConfig(); + const { + 'input-parameters': inputParameters, + 'output-parameter': outputParameter, + } = safeConfig; return inputs.map(input => { - validateSingleInput(input, inputParameters); + const safeInput = validateSingleInput(input, inputParameters); + const calculatedResult = calculateProduct(safeInput, inputParameters); const result = { ...input, - [outputParameter]: calculateProduct(input, inputParameters), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); diff --git a/src/if-run/builtins/regex/index.ts b/src/if-run/builtins/regex/index.ts index 5e65e677..171f4a8a 100644 --- a/src/if-run/builtins/regex/index.ts +++ b/src/if-run/builtins/regex/index.ts @@ -64,8 +64,8 @@ export const Regex = ( * Executes the regex of the given parameter. */ const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const {parameter: parameter, match, output} = safeGlobalConfig; + const safeConfig = validateConfig(); + const {parameter: parameter, match, output} = safeConfig; return inputs.map(input => { const safeInput = Object.assign( diff --git a/src/if-run/builtins/sci-embodied/index.ts b/src/if-run/builtins/sci-embodied/index.ts index 835284ea..a4451cc6 100644 --- a/src/if-run/builtins/sci-embodied/index.ts +++ b/src/if-run/builtins/sci-embodied/index.ts @@ -1,5 +1,11 @@ import {z, ZodType} from 'zod'; +import { + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapInputIfNeeded, @@ -101,28 +107,63 @@ export const SciEmbodied = ( /** * Checks for required fields in input. */ - const validateConfig = () => { + const validateConfig = (input: PluginParams) => { const schema = z.object({ - 'baseline-vcpus': z.number().gte(0).default(1), - 'baseline-memory': z.number().gte(0).default(16), - 'baseline-emissions': z.number().gte(0).default(1000000), - lifespan: z.number().gt(0).default(126144000), - 'vcpu-emissions-constant': z.number().gte(0).default(100000), - 'memory-emissions-constant': z - .number() - .gte(0) - .default(533 / 384), - 'ssd-emissions-constant': z.number().gte(0).default(50000), - 'hdd-emissions-constant': z.number().gte(0).default(100000), - 'gpu-emissions-constant': z.number().gte(0).default(150000), + 'baseline-vcpus': z.preprocess( + value => validateArithmeticExpression('baseline-vcpus', value), + z.number().gte(0).default(1) + ), + 'baseline-memory': z.preprocess( + value => validateArithmeticExpression('baseline-memory', value), + z.number().gte(0).default(16) + ), + 'baseline-emissions': z.preprocess( + value => validateArithmeticExpression('baseline-emissions', value), + z.number().gte(0).default(1000000) + ), + lifespan: z.preprocess( + value => validateArithmeticExpression('lifespan', value), + z.number().gt(0).default(126144000) + ), + 'vcpu-emissions-constant': z.preprocess( + value => validateArithmeticExpression('vcpu-emissions-constant', value), + z.number().gte(0).default(100000) + ), + 'memory-emissions-constant': z.preprocess( + value => + validateArithmeticExpression('memory-emissions-constant', value), + z + .number() + .gte(0) + .default(533 / 384) + ), + 'ssd-emissions-constant': z.preprocess( + value => validateArithmeticExpression('ssd-emissions-constant', value), + z.number().gte(0).default(50000) + ), + 'hdd-emissions-constant': z.preprocess( + value => validateArithmeticExpression('hdd-emissions-constant', value), + z.number().gte(0).default(100000) + ), + 'gpu-emissions-constant': z.preprocess( + value => validateArithmeticExpression('gpu-emissions-constant', value), + z.number().gte(0).default(150000) + ), 'output-parameter': z.string().optional(), }); const mappedConfig = mapConfigIfNeeded(config, mapping); + const evaluatedConfig = evaluateConfig({ + config: mappedConfig, + input, + parametersToEvaluate: Object.keys(config).filter( + key => key !== 'output-parameter' + ), + }); return validate>( schema as ZodType, - mappedConfig + evaluatedConfig ); }; @@ -141,7 +182,11 @@ export const SciEmbodied = ( time: z.number().gt(0).optional(), }); - return validate>(schema as ZodType, input); + const evaluatedInput = evaluateInput(input); + return validate>( + schema as ZodType, + evaluatedInput + ); }; /** @@ -150,9 +195,8 @@ export const SciEmbodied = ( * 3. Calculates total embodied carbon by substracting and the difference between baseline server and given one. */ const execute = (inputs: PluginParams[]) => { - const safeConfig = validateConfig(); - return inputs.map(input => { + const safeConfig = validateConfig(input); const mappedInput = mapInputIfNeeded(input, mapping); const safeInput = validateInput(mappedInput); @@ -181,9 +225,13 @@ export const SciEmbodied = ( const embodiedCarbonKey = safeConfig['output-parameter'] || 'embodied-carbon'; + const result = { ...input, - [embodiedCarbonKey]: totalEmbodiedScaledByUsageAndTime, + ...evaluateArithmeticOutput( + embodiedCarbonKey, + totalEmbodiedScaledByUsageAndTime + ), }; return mapOutputIfNeeded(result, mapping); diff --git a/src/if-run/builtins/sci/index.ts b/src/if-run/builtins/sci/index.ts index 81fd62d0..7e98a8f0 100644 --- a/src/if-run/builtins/sci/index.ts +++ b/src/if-run/builtins/sci/index.ts @@ -1,7 +1,15 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, + getParameterFromArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapInputIfNeeded, + mapConfigIfNeeded, mapOutputIfNeeded, } from '@grnsft/if-core/utils/helpers'; import { @@ -77,55 +85,76 @@ export const Sci = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); const schema = z .object({ - 'functional-unit': z.string(), + 'functional-unit': z + .string() + .refine(param => + validateArithmeticExpression('functional-unit', param) + ), }) .refine(data => data['functional-unit'], { message: MISSING_FUNCTIONAL_UNIT_CONFIG, }); - return validate>(schema, config); + return validate>(schema, mappedConfig); }; /** * Calculate the total emissions for a list of inputs. */ const execute = (inputs: PluginParams[]): PluginParams[] => { + const safeConfig = validateConfig(); + return inputs.map((input, index) => { - const mappedInput = mapInputIfNeeded(input, mapping); - const safeInput = validateInput(mappedInput); - const functionalUnit = input[config['functional-unit']]; + const safeInput = Object.assign( + {}, + input, + validateInput(input, safeConfig) + ); + + const evaluatedConfig = evaluateConfig({ + config: safeConfig, + input: safeInput, + parametersToEvaluate: ['functional-unit'], + }); + const functionalUnit = isNaN(evaluatedConfig['functional-unit']) + ? safeInput[evaluatedConfig['functional-unit']] + : evaluatedConfig['functional-unit']; if (functionalUnit === 0) { console.warn(ZERO_DIVISION(Sci.name, index)); return { ...input, + ...safeInput, sci: safeInput['carbon'], }; } + const calculatedResult = safeInput['carbon'] / functionalUnit; const result = { ...input, - sci: safeInput['carbon'] / functionalUnit, + ...safeInput, + ...evaluateArithmeticOutput('sci', calculatedResult), }; return mapOutputIfNeeded(result, mapping); }); }; + /** * Checks for fields in input. */ - const validateInput = (input: PluginParams) => { - const validatedConfig = validateConfig(); - - if ( - !( - validatedConfig['functional-unit'] in input && - input[validatedConfig['functional-unit']] >= 0 - ) - ) { + const validateInput = (input: PluginParams, safeConfig: ConfigParams) => { + const mappedInput = mapInputIfNeeded(input, mapping); + + const functionalUnit = getParameterFromArithmeticExpression( + safeConfig['functional-unit'] + ); + + if (!(functionalUnit in mappedInput && mappedInput[functionalUnit] >= 0)) { throw new MissingInputDataError(MISSING_FUNCTIONAL_UNIT_INPUT); } @@ -138,7 +167,9 @@ export const Sci = ( message: SCI_MISSING_FN_UNIT(config['functional-unit']), }); - return validate>(schema, input); + const evaluatedInput = evaluateInput(mappedInput); + + return validate>(schema, evaluatedInput); }; return { diff --git a/src/if-run/builtins/subtract/index.ts b/src/if-run/builtins/subtract/index.ts index 5051017c..8d452464 100644 --- a/src/if-run/builtins/subtract/index.ts +++ b/src/if-run/builtins/subtract/index.ts @@ -1,5 +1,11 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -42,7 +48,12 @@ export const Subtract = ( const configSchema = z.object({ 'input-parameters': z.array(z.string()), - 'output-parameter': z.string().min(1), + 'output-parameter': z + .string() + .min(1) + .refine(value => + validateArithmeticExpression('output-parameter', value) + ), }); return validate>(configSchema, mappedConfig); @@ -55,9 +66,11 @@ export const Subtract = ( input: PluginParams, inputParameters: string[] ) => { + const evaluatedInput = evaluateInput(input); + const inputData = inputParameters.reduce( (acc, param) => { - acc[param] = input[param]; + acc[param] = evaluatedInput[param]; return acc; }, @@ -66,26 +79,39 @@ export const Subtract = ( const validationSchema = z.record(z.string(), z.number()); - validate(validationSchema, inputData); - - return input; + return validate(validationSchema, inputData); }; /** * Subtract items from inputParams[1..n] from inputParams[0] and write the result in a new param outputParam. */ const execute = (inputs: PluginParams[]): PluginParams[] => { + const safeConfig = validateConfig(); const { 'input-parameters': inputParameters, 'output-parameter': outputParameter, - } = validateConfig(); + } = safeConfig; return inputs.map(input => { - validateSingleInput(input, inputParameters); + const calculatedConfig = evaluateConfig({ + config: safeConfig, + input, + parametersToEvaluate: safeConfig['input-parameters'], + }); + const safeInput = Object.assign( + {}, + input, + validateSingleInput(input, inputParameters) + ); + const calculatedResult = calculateDiff( + safeInput, + calculatedConfig['input-parameters'] || inputParameters + ); const result = { ...input, - [outputParameter]: calculateDiff(input, inputParameters), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); diff --git a/src/if-run/builtins/sum/index.ts b/src/if-run/builtins/sum/index.ts index 580821dd..e111bce4 100644 --- a/src/if-run/builtins/sum/index.ts +++ b/src/if-run/builtins/sum/index.ts @@ -1,5 +1,11 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -34,16 +40,30 @@ export const Sum = ( * Calculate the sum of each input-paramters. */ const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const inputParameters = safeGlobalConfig['input-parameters']; - const outputParameter = safeGlobalConfig['output-parameter']; + const safeConfig = validateConfig(); + const { + 'input-parameters': inputParameters, + 'output-parameter': outputParameter, + } = safeConfig; return inputs.map(input => { - validateSingleInput(input, inputParameters); + const safeInput = validateSingleInput(input, inputParameters); + + const calculatedConfig = evaluateConfig({ + config: safeConfig, + input, + parametersToEvaluate: config['input-parameters'], + }); + + const calculatedResult = calculateSum( + safeInput, + calculatedConfig['input-parameters'] || inputParameters + ); const result = { ...input, - [outputParameter]: calculateSum(input, inputParameters), + ...safeInput, + ...evaluateArithmeticOutput(outputParameter, calculatedResult), }; return mapOutputIfNeeded(result, mapping); @@ -62,7 +82,12 @@ export const Sum = ( const configSchema = z.object({ 'input-parameters': z.array(z.string()), - 'output-parameter': z.string().min(1), + 'output-parameter': z + .string() + .min(1) + .refine(value => + validateArithmeticExpression('output-parameter', value) + ), }); return validate>(configSchema, mappedConfig); @@ -75,18 +100,17 @@ export const Sum = ( input: PluginParams, inputParameters: string[] ) => { + const evaluatedInput = evaluateInput(input); const inputData = inputParameters.reduce( (acc, param) => { - acc[param] = input[param]; + acc[param] = evaluatedInput[param]; return acc; }, {} as Record ); const validationSchema = z.record(z.string(), z.number()); - validate(validationSchema, inputData); - - return input; + return validate(validationSchema, inputData); }; /** diff --git a/src/if-run/builtins/time-converter/index.ts b/src/if-run/builtins/time-converter/index.ts index c4b9ef57..8e82458c 100644 --- a/src/if-run/builtins/time-converter/index.ts +++ b/src/if-run/builtins/time-converter/index.ts @@ -1,5 +1,12 @@ import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import { + ERRORS, + evaluateInput, + evaluateConfig, + evaluateArithmeticOutput, + validateArithmeticExpression, + getParameterFromArithmeticExpression, +} from '@grnsft/if-core/utils'; import { mapConfigIfNeeded, mapOutputIfNeeded, @@ -33,16 +40,31 @@ export const TimeConverter = ( }; const execute = (inputs: PluginParams[]) => { - const safeGlobalConfig = validateConfig(); - const inputParameter = safeGlobalConfig['input-parameter']; - const outputParameter = safeGlobalConfig['output-parameter']; + const safeConfig = validateConfig(); + const { + 'input-parameter': inputParameter, + 'output-parameter': outputParameter, + } = safeConfig; return inputs.map(input => { - validateInput(input, inputParameter); + const safeInput = Object.assign( + {}, + input, + validateInput(input, inputParameter) + ); + const calculatedConfig = evaluateConfig({ + config: safeConfig, + input: safeInput, + parametersToEvaluate: ['input-parameter'], + }); const result = { ...input, - [outputParameter]: calculateEnergy(input), + ...safeInput, + ...evaluateArithmeticOutput( + outputParameter, + calculateEnergy(safeInput, calculatedConfig['input-parameter']) + ), }; return mapOutputIfNeeded(result, mapping); @@ -52,14 +74,20 @@ export const TimeConverter = ( /** * Calculates the energy for given period. */ - const calculateEnergy = (input: PluginParams) => { + const calculateEnergy = ( + input: PluginParams, + inputParameter: string | number + ) => { const originalTimeUnit = config['original-time-unit']; const originalTimeUnitInSeoncds = TIME_UNITS_IN_SECONDS[originalTimeUnit]; - const energyPerPeriod = input[config['input-parameter']]; + const energyPerPeriod = isNaN(Number(inputParameter)) + ? input[inputParameter] + : inputParameter; const newTimeUnit = config['new-time-unit'] === 'duration' ? input.duration : TIME_UNITS_IN_SECONDS[config['new-time-unit']]; + const result = (energyPerPeriod / originalTimeUnitInSeoncds) * newTimeUnit; return Number(result.toFixed(6)); @@ -68,13 +96,17 @@ export const TimeConverter = ( /** * Checks for required fields in input. */ - const validateInput = (input: PluginParams, inputParameter: string) => { + const validateInput = (input: PluginParams, configInputParameter: string) => { + const inputParameter = + getParameterFromArithmeticExpression(configInputParameter); + const schema = z.object({ duration: z.number().gte(1), [inputParameter]: z.number(), }); - return validate>(schema, input); + const evaluatedInput = evaluateInput(input); + return validate>(schema, evaluatedInput); }; /** @@ -93,12 +125,20 @@ export const TimeConverter = ( ] as const; const originalTimeUnitValues = timeUnitsValues as [string, ...string[]]; - const configSchema = z.object({ - 'input-parameter': z.string(), - 'original-time-unit': z.enum(originalTimeUnitValues), - 'new-time-unit': z.enum(originalTimeUnitValuesWithDuration), - 'output-parameter': z.string().min(1), - }); + const configSchema = z + .object({ + 'input-parameter': z.string(), + 'original-time-unit': z.enum(originalTimeUnitValues), + 'new-time-unit': z.enum(originalTimeUnitValuesWithDuration), + 'output-parameter': z.string().min(1), + }) + .refine(params => { + Object.entries(params).forEach(([param, value]) => + validateArithmeticExpression(param, value) + ); + + return true; + }); return validate>(configSchema, config); }; diff --git a/src/if-run/builtins/time-sync/index.ts b/src/if-run/builtins/time-sync/index.ts index e34c4ca3..5e3b56e0 100644 --- a/src/if-run/builtins/time-sync/index.ts +++ b/src/if-run/builtins/time-sync/index.ts @@ -2,7 +2,7 @@ import {isDate} from 'node:util/types'; import {Settings, DateTime, DateTimeMaybeValid, Interval} from 'luxon'; import {z} from 'zod'; -import {ERRORS} from '@grnsft/if-core/utils'; +import {ERRORS, evaluateInput} from '@grnsft/if-core/utils'; import { mapInputIfNeeded, mapOutputIfNeeded, @@ -123,14 +123,14 @@ export const TimeSync = ( /** Checks for timestamps overlap. */ if ( parseDate(previousInput.timestamp).plus({ - seconds: previousInput.duration, + seconds: eval(previousInput.duration), }) > currentMoment ) { throw new InvalidInputError(INVALID_OBSERVATION_OVERLAP); } const compareableTime = previousInputTimestamp.plus({ - seconds: previousInput.duration, + seconds: eval(previousInput.duration), }); const timelineGapSize = currentMoment @@ -143,14 +143,14 @@ export const TimeSync = ( ...getZeroishInputPerSecondBetweenRange( compareableTime, currentMoment, - input + safeInput ) ); } } /** Break down current observation. */ - for (let i = 0; i < input.duration; i++) { - const normalizedInput = breakDownInput(input, i); + for (let i = 0; i < safeInput.duration; i++) { + const normalizedInput = breakDownInput(safeInput, i); acc.push(normalizedInput); } @@ -205,7 +205,9 @@ export const TimeSync = ( duration: z.number(), }); - return validate>(schema, input); + const evaluatedInput = evaluateInput(input); + + return validate>(schema, evaluatedInput); }; /** @@ -252,7 +254,8 @@ export const TimeSync = ( * Breaks down input per minimal time unit. */ const breakDownInput = (input: PluginParams, i: number) => { - const metrics = Object.keys(input); + const evaluatedInput = evaluateInput(input); + const metrics = Object.keys(evaluatedInput); return metrics.reduce((acc, metric) => { const aggregationParams = getAggregationInfoFor(metric); @@ -279,8 +282,11 @@ export const TimeSync = ( acc[metric] = aggregationParams.time === 'sum' - ? convertPerInterval(input[metric], input['duration']) - : input[metric]; + ? convertPerInterval( + evaluatedInput[metric], + evaluatedInput['duration'] + ) + : evaluatedInput[metric]; return acc; }, {} as PluginParams); @@ -372,7 +378,7 @@ export const TimeSync = ( const lastInput = inputs[inputs.length - 1]; const endDiffInSeconds = parseDate(lastInput.timestamp) - .plus({second: lastInput.duration}) + .plus({second: eval(lastInput.duration)}) .diff(params.endTime) .as('seconds'); @@ -486,7 +492,7 @@ export const TimeSync = ( if (end) { const lastInput = inputs[inputs.length - 1]; const lastInputEnd = parseDate(lastInput.timestamp).plus({ - seconds: lastInput.duration, + seconds: eval(lastInput.duration), }); paddedArray.push( ...getZeroishInputPerSecondBetweenRange(