diff --git a/src/__tests__/unit/builtins/CommonGenerator.test.ts b/src/__tests__/unit/builtins/CommonGenerator.test.ts new file mode 100644 index 000000000..881aa810e --- /dev/null +++ b/src/__tests__/unit/builtins/CommonGenerator.test.ts @@ -0,0 +1,44 @@ +import {KeyValuePair} from '../../../types/common'; + +import {ERRORS} from '../../../util/errors'; + +import {CommonGenerator} from '../../../builtins/mock-observations/helpers/common-generator'; + +const {InputValidationError} = ERRORS; + +describe('lib/mock-observations/CommonGenerator: ', () => { + describe('initialize: ', () => { + it('throws an error when config is not empty object.', async () => { + const commonGenerator = CommonGenerator({}); + + expect.assertions(1); + + try { + commonGenerator.next([]); + } catch (error) { + expect(error).toEqual( + new InputValidationError( + 'CommonGenerator: Config must not be null or empty.' + ) + ); + } + }); + }); + + describe('next(): ', () => { + it('returns a result with valid data.', async () => { + const config: KeyValuePair = { + key1: 'value1', + key2: 'value2', + }; + const commonGenerator = CommonGenerator(config); + + expect.assertions(1); + + expect(commonGenerator.next([])).toStrictEqual({ + key1: 'value1', + key2: 'value2', + }); + }); + }); +}); diff --git a/src/__tests__/unit/builtins/RandIntGenerator.test.ts b/src/__tests__/unit/builtins/RandIntGenerator.test.ts new file mode 100644 index 000000000..c904f7350 --- /dev/null +++ b/src/__tests__/unit/builtins/RandIntGenerator.test.ts @@ -0,0 +1,71 @@ +import {KeyValuePair} from '../../../types/common'; + +import {ERRORS} from '../../../util/errors'; + +import {RandIntGenerator} from '../../../builtins/mock-observations/helpers/rand-int-generator'; + +const {InputValidationError} = ERRORS; + +describe('lib/mock-observations/RandIntGenerator: ', () => { + describe('initialize', () => { + it('throws an error when the generator name is empty string.', async () => { + expect.assertions(1); + try { + RandIntGenerator('', {}); + } catch (error) { + expect(error).toEqual( + new InputValidationError( + 'RandIntGenerator: `name` is empty or all spaces.' + ) + ); + } + }); + + it('throws an error when config is empty object.', async () => { + expect.assertions(1); + try { + RandIntGenerator('generator-name', {}); + } catch (error) { + expect(error).toEqual( + new InputValidationError( + 'RandIntGenerator: Config must not be null or empty.' + ) + ); + } + }); + + it('throws an error `min` is missing from the config.', async () => { + const config = {max: 90}; + + expect.assertions(1); + + try { + RandIntGenerator('random', config); + } catch (error) { + expect(error).toEqual( + new InputValidationError( + 'RandIntGenerator: Config is missing min or max.' + ) + ); + } + }); + }); + + describe('next(): ', () => { + it('returns a result with valid data.', async () => { + const config: KeyValuePair = { + min: 10, + max: 90, + }; + const randIntGenerator = RandIntGenerator('random', config); + const result = randIntGenerator.next([]) as {random: number}; + + expect.assertions(4); + + expect(result).toBeInstanceOf(Object); + expect(result).toHaveProperty('random'); + expect(result.random).toBeGreaterThanOrEqual(10); + expect(result.random).toBeLessThanOrEqual(90); + }); + }); +}); diff --git a/src/__tests__/unit/builtins/mock-observations.test.ts b/src/__tests__/unit/builtins/mock-observations.test.ts new file mode 100644 index 000000000..37d3a04b4 --- /dev/null +++ b/src/__tests__/unit/builtins/mock-observations.test.ts @@ -0,0 +1,323 @@ +import {MockObservations} from '../../../builtins/mock-observations'; + +import {ERRORS} from '../../../util/errors'; + +const {InputValidationError} = ERRORS; + +describe('lib/mock-observations: ', () => { + describe('init: ', () => { + it('successfully initalized.', () => { + const mockObservations = MockObservations({ + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }); + + expect(mockObservations).toHaveProperty('metadata'); + expect(mockObservations).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('executes successfully.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 11}, + }, + }, + }; + const mockObservations = MockObservations(config); + const result = await mockObservations.execute([]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2023-07-06T00:00:00.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'A1', + region: 'uk-west', + 'cpu/utilization': 10, + }, + { + timestamp: '2023-07-06T00:00:30.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'A1', + region: 'uk-west', + 'cpu/utilization': 10, + }, + { + timestamp: '2023-07-06T00:00:00.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'B1', + region: 'uk-west', + 'cpu/utilization': 10, + }, + { + timestamp: '2023-07-06T00:00:30.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'B1', + region: 'uk-west', + 'cpu/utilization': 10, + }, + ]); + }); + + it('throws an error when the `min` is greater then `max` of `randint` config.', async () => { + const errorMessage = + 'RandIntGenerator: Min value should not be greater than or equal to max value of cpu/utilization.'; + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 20, max: 11}, + }, + }, + }; + + expect.assertions(2); + + const mockObservations = MockObservations(config); + try { + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual(new InputValidationError(errorMessage)); + } + }); + + it('throws when `generators` are not provided.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + }; + + expect.assertions(2); + + try { + const mockObservations = MockObservations(config); + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"generators" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + + it('throws when `components` are not provided.', async () => { + const errorMessage = + '"components" parameter is required. Error code: invalid_type.'; + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }; + + expect.assertions(2); + + try { + const mockObservations = MockObservations(config); + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual(new InputValidationError(errorMessage)); + } + }); + + it('throws when `duration` is not provided.', async () => { + expect.assertions(2); + + try { + const mockObservations = MockObservations({ + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }); + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"duration" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + + it('throws when `timestamp-to` is not provided.', async () => { + expect.assertions(2); + + try { + const mockObservations = MockObservations({ + 'timestamp-from': '2023-07-06T00:00', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }); + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"timestamp-to" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + + it('throws when `timestamp-from` is missing.', async () => { + expect.assertions(2); + + try { + const mockObservations = MockObservations({ + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }); + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"timestamp-from" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when `randInt` is not valid.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: null, + }, + }; + const mockObservations = MockObservations(config); + + expect.assertions(2); + + try { + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"generators.randint" parameter is expected object, received null. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when `common` is not valid.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: null, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }; + const mockObservations = MockObservations(config); + + expect.assertions(2); + + try { + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"generators.common" parameter is expected object, received null. Error code: invalid_type.' + ) + ); + } + }); + }); +}); diff --git a/src/builtins/index.ts b/src/builtins/index.ts index 492713ee5..355d91fd9 100644 --- a/src/builtins/index.ts +++ b/src/builtins/index.ts @@ -1,5 +1,6 @@ export {GroupBy} from './group-by'; export {TimeSync} from './time-sync'; +export {MockObservations} from './mock-observations'; export {Divide} from './divide'; export {Subtract} from './subtract'; export {Coefficient} from './coefficient'; diff --git a/src/builtins/mock-observations/README.md b/src/builtins/mock-observations/README.md new file mode 100644 index 000000000..629e27f2d --- /dev/null +++ b/src/builtins/mock-observations/README.md @@ -0,0 +1,97 @@ +# Mock Observations Plugin + +## Introduction + +A plugin for mocking observations (inputs) for testing and demo purposes + +## Scope + +The mode currently mocks 2 types of observation data: + +- Common key-value pairs, that are generated statically and are the same for each generated observation/input (see 'helpers/CommonGenerator.ts') +- Randomly generated integer values for predefined keys (see 'helpers/RandIntGenerator.ts') + +### Plugin global config + +- `timestamp-from`, `timestamp-to` and `duration` define time buckets for which to generate observations. +- `generators` define which fields to generate for each observation +- `components` define the components for which to generate observations. The observations generated according to `timestamp-from`, `timestamp-to`, `duration` and `generators` will be duplicated for each component. + +### Authentication + +N/A + +### Inputs + +The plugin's `global-config` section in the manifest file determines its behaviour. +'inputs' section is ignored. + +### Typescript Usage + +```typescript +const mockObservations = MockObservations({ + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:10', + duration: 60, + components: { + 'instance-type': 'A1', + }, + generators: { + common: { + region: 'uk-west', + }, + }, +}); +const result = await mockObservations.execute([]); +``` + +### manifest Example + +IF users will typically call the plugin as part of a pipeline defined in a `manifest` file. In this case, instantiating the plugin is handled by `ie` and does not have to be done explicitly by the user. The following is an example `manifest` that calls `mock-observation`: + +```yaml +name: mock-observation-demo +description: example invoking mock-observation plugin +tags: +initialize: + outputs: + - yaml + plugins: + mock-observations: + kind: plugin + method: MockObservations + path: 'builtin' + global-config: + timestamp-from: 2023-07-06T00:00 + timestamp-to: 2023-07-06T00:10 + duration: 60 + components: + - instance-type: A1 + - instance-type: B1 + generators: + common: + region: uk-west + common-key: common-val + randint: + cpu/utilization: + min: 1 + max: 99 + memory/utilization: + min: 1 + max: 99 +tree: + children: + child: + pipeline: + - mock-observations + inputs: +``` + +You can run this example `manifest` by saving it as `manifests/plugins/mock-observation.yml` and executing the following command from the project root: + +```sh +npm i -g @grnsft/if +ie --manifest ./examples/manifests/test/mock-observation.yml --output ./examples/outputs/mock-observation +``` + +The results will be saved to a new `yaml` file in `./examples/outputs`. diff --git a/src/builtins/mock-observations/helpers/common-generator.ts b/src/builtins/mock-observations/helpers/common-generator.ts new file mode 100644 index 000000000..24bea4c94 --- /dev/null +++ b/src/builtins/mock-observations/helpers/common-generator.ts @@ -0,0 +1,39 @@ +import {KeyValuePair} from '../../../types/common'; +import {ERRORS} from '../../../util/errors'; +import {buildErrorMessage} from '../../../util/helpers'; + +import {Generator} from '../interfaces'; + +const {InputValidationError} = ERRORS; + +export const CommonGenerator = (config: KeyValuePair): Generator => { + const errorBuilder = buildErrorMessage(CommonGenerator.name); + + /** + * Creates new copy of the given `object`. + */ + const copyObject = (object: T): T => ({...object}); + + /** + * Validates the provided config is not null or empty. + * returns a copy of the validated config, otherwise throws an InputValidationError. + */ + const validateConfig = (config: object) => { + if (!config || Object.keys(config).length === 0) { + throw new InputValidationError( + errorBuilder({message: 'Config must not be null or empty'}) + ); + } + + return copyObject(config); + }; + + /** + * Generates next value by copying the validated config. + */ + const next = (): Object => copyObject(validateConfig(config)); + + return { + next, + }; +}; diff --git a/src/builtins/mock-observations/helpers/rand-int-generator.ts b/src/builtins/mock-observations/helpers/rand-int-generator.ts new file mode 100644 index 000000000..e27d694c3 --- /dev/null +++ b/src/builtins/mock-observations/helpers/rand-int-generator.ts @@ -0,0 +1,82 @@ +import {KeyValuePair} from '../../../types/common'; +import {ERRORS} from '../../../util/errors'; +import {buildErrorMessage} from '../../../util/helpers'; + +import {Generator} from '../interfaces'; +import {RandIntGeneratorParams} from '../types'; + +const {InputValidationError} = ERRORS; + +export const RandIntGenerator = ( + name: string, + config: KeyValuePair +): Generator => { + const errorBuilder = buildErrorMessage(RandIntGenerator.name); + + const next = () => ({ + [validatedName]: generateRandInt(getFieldToPopulate()), + }); + + const validateName = (name: string | null): string => { + if (!name || name.trim() === '') { + throw new InputValidationError( + errorBuilder({ + message: '`name` is empty or all spaces', + }) + ); + } + return name; + }; + + const validateConfig = (config: KeyValuePair): {min: number; max: number} => { + if (!config || Object.keys(config).length === 0) { + throw new InputValidationError( + errorBuilder({ + message: 'Config must not be null or empty', + }) + ); + } + + if (!config.min || !config.max) { + throw new InputValidationError( + errorBuilder({ + message: 'Config is missing min or max', + }) + ); + } + + if (config.min >= config.max) { + throw new InputValidationError( + errorBuilder({ + message: `Min value should not be greater than or equal to max value of ${validatedName}`, + }) + ); + } + return {min: config.min, max: config.max}; + }; + + const validatedName = validateName(name); + const validatedConfig = validateConfig(config); + + const getFieldToPopulate = () => { + return { + name: validatedName, + min: validatedConfig.min, + max: validatedConfig.max, + }; + }; + + const generateRandInt = ( + randIntGenerator: RandIntGeneratorParams + ): number => { + const randomNumber = Math.random(); + const scaledNumber = + randomNumber * (randIntGenerator.max - randIntGenerator.min) + + randIntGenerator.min; + return Math.trunc(scaledNumber); + }; + + return { + next, + }; +}; diff --git a/src/builtins/mock-observations/index.ts b/src/builtins/mock-observations/index.ts new file mode 100644 index 000000000..2d65f4643 --- /dev/null +++ b/src/builtins/mock-observations/index.ts @@ -0,0 +1,176 @@ +import {DateTime, Duration} from 'luxon'; +import {z} from 'zod'; + +import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ConfigParams, KeyValuePair} from '../../types/common'; + +import {validate} from '../../util/validations'; + +import {CommonGenerator} from './helpers/common-generator'; +import {RandIntGenerator} from './helpers/rand-int-generator'; +import {Generator} from './interfaces/index'; +import {ObservationParams} from './types'; + +export const MockObservations = (globalConfig: ConfigParams): ExecutePlugin => { + const metadata = { + kind: 'execute', + }; + + /** + * Generate sets of mocked observations based on config. + */ + const execute = (inputs: PluginParams[]) => { + const {duration, timeBuckets, components, generators} = + generateParamsFromConfig(); + const generatorToHistory = new Map(); + + generators.forEach(generator => { + generatorToHistory.set(generator, []); + }); + + const defaults = inputs && inputs[0]; + + return Object.entries(components).reduce((acc: PluginParams[], item) => { + const component = item[1]; + timeBuckets.forEach(timeBucket => { + const observation = createObservation( + {duration, component, timeBucket, generators}, + generatorToHistory + ); + + acc.push(Object.assign({}, defaults, observation)); + }); + + return acc; + }, []); + }; + + /** + * Validates global config parameters. + */ + const validateGlobalConfig = () => { + const schema = z.object({ + 'timestamp-from': z.string(), + 'timestamp-to': z.string(), + duration: z.number(), + components: z.array(z.record(z.string())), + generators: z.object({ + common: z.record(z.string().or(z.number())), + randint: z.record(z.object({min: z.number(), max: z.number()})), + }), + }); + + return validate>(schema, globalConfig); + }; + + /** + * Configures the MockObservations Plugin for IF + */ + const generateParamsFromConfig = () => { + const { + 'timestamp-from': timestampFrom, + 'timestamp-to': timestampTo, + duration, + generators, + components, + } = validateGlobalConfig(); + const convertedTimestampFrom = DateTime.fromISO(timestampFrom, { + zone: 'UTC', + }); + const convertedTimestampTo = DateTime.fromISO(timestampTo, {zone: 'UTC'}); + + return { + duration, + timeBuckets: createTimeBuckets( + convertedTimestampFrom, + convertedTimestampTo, + duration + ), + generators: createGenerators(generators), + components, + }; + }; + + /* + * create time buckets based on start time, end time and duration of each bucket. + */ + const createTimeBuckets = ( + timestampFrom: DateTime, + timestampTo: DateTime, + duration: number, + timeBuckets: DateTime[] = [] + ): DateTime[] => { + if ( + timestampFrom < timestampTo || + timestampFrom.plus(Duration.fromObject({seconds: duration})) < timestampTo + ) { + return createTimeBuckets( + timestampFrom.plus(Duration.fromObject({seconds: duration})), + timestampTo, + duration, + [...timeBuckets, timestampFrom] + ); + } + return timeBuckets; + }; + + /* + * create generators based on a given config + */ + const createGenerators = (generatorsConfig: object): Generator[] => { + const createCommonGenerator = (config: any): Generator[] => [ + CommonGenerator(config), + ]; + + const createRandIntGenerators = (config: any): Generator[] => { + return Object.entries(config).map(([fieldToPopulate, value]) => + RandIntGenerator(fieldToPopulate, value as KeyValuePair) + ); + }; + + return Object.entries(generatorsConfig).flatMap(([key, value]) => + key === 'randint' + ? createRandIntGenerators(value).flat() + : createCommonGenerator(value) + ); + }; + + /* + * Creates time buckets based on start time, end time and duration of each bucket. + */ + const createObservation = ( + observationParams: ObservationParams, + generatorToHistory: Map + ): PluginParams => { + const {duration, component, timeBucket, generators} = observationParams; + const timestamp = timeBucket.toISO(); + + const generateObservation = (generator: Generator) => { + const history = generatorToHistory.get(generator) || []; + const generated: Record = generator.next(history); + + generatorToHistory.set(generator, [...history, generated.value]); + + return generated; + }; + + const generateObservations = (gen: Generator) => generateObservation(gen); + const generatedValues = generators.map(generateObservations); + const initialObservation: PluginParams = { + timestamp, + duration, + ...component, + }; + const generatedObservation = generatedValues.reduce( + (observation, generated) => Object.assign(observation, generated), + initialObservation + ); + + return generatedObservation as PluginParams; + }; + + return { + metadata, + execute, + }; +}; diff --git a/src/builtins/mock-observations/interfaces/index.ts b/src/builtins/mock-observations/interfaces/index.ts new file mode 100644 index 000000000..4efae5f59 --- /dev/null +++ b/src/builtins/mock-observations/interfaces/index.ts @@ -0,0 +1,6 @@ +export type Generator = { + /** + * generate the next value, optionally based on historical values + */ + next: (historical: Object[] | undefined) => Object; +}; diff --git a/src/builtins/mock-observations/types.ts b/src/builtins/mock-observations/types.ts new file mode 100644 index 000000000..82591207c --- /dev/null +++ b/src/builtins/mock-observations/types.ts @@ -0,0 +1,16 @@ +import {DateTime} from 'luxon'; + +import {Generator} from './interfaces/index'; + +export type ObservationParams = { + duration: number; + timeBucket: DateTime; + component: Record; + generators: Generator[]; +}; + +export type RandIntGeneratorParams = { + name: string; + min: number; + max: number; +}; diff --git a/src/types/common.ts b/src/types/common.ts index e91feef71..f7d337392 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1 +1,5 @@ +export type KeyValuePair = { + [key: string]: any; +}; + export type ConfigParams = Record; diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 400c61ab3..3252fbeb7 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -7,6 +7,16 @@ import {STRINGS} from '../config'; const {ISSUE_TEMPLATE} = STRINGS; +/** + * Formats given error according to class instance, scope and message. + */ +export const buildErrorMessage = + (classInstanceName: string) => (params: ErrorFormatParams) => { + const {scope, message} = params; + + return `${classInstanceName}${scope ? `(${scope})` : ''}: ${message}.`; + }; + /** * Impact engine error handler. Logs errors and appends issue template if error is unknown. */ @@ -39,13 +49,6 @@ export const mergeObjects = (defaults: any, input: any) => { return merged; }; -export const buildErrorMessage = - (classInstanceName: string) => (params: ErrorFormatParams) => { - const {scope, message} = params; - - return `${classInstanceName}${scope ? `(${scope})` : ''}: ${message}.`; - }; - /** * Promise version of Node's `exec` from `child-process`. */