diff --git a/tools/src/helpers.ts b/tools/src/helpers.ts index 4144afd3..d338f796 100644 --- a/tools/src/helpers.ts +++ b/tools/src/helpers.ts @@ -160,3 +160,21 @@ export function write_json (file_path: string, content: any, replacer?: (this: a export async function sleep (ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)) } + +/** + * Returns a string split using a delimiter, but only up to a certain number of times, + * returning the remainder of the string as the last element if the result. + * + * @param str a string + * @param delim delimiter + * @param count max number of splits + * @returns an array of strings + */ +export function split(str: any, delim: string, count: number = 0): string[] { + if (str === undefined) return [] + const parts = str.split(delim) + if (count <= 0 || parts.length <= count) return parts + const result = parts.slice(0, count - 1) + result.push(parts.slice(count - 1).join(delim)) + return result +} diff --git a/tools/src/tester/OutputReference.ts b/tools/src/tester/OutputReference.ts new file mode 100644 index 00000000..4c47212a --- /dev/null +++ b/tools/src/tester/OutputReference.ts @@ -0,0 +1,57 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { split } from "helpers" + +export class OutputReference { + chapter_id: string + output_name: string|undefined + + constructor(chapter_id: string, output_name: string | undefined = undefined) { + this.chapter_id = chapter_id + this.output_name = output_name + } + + /** + * Parses a string and returns a collection of output references that it may contain. + * + * @param str a string + * @returns an array of output references + */ + static parse(str: string): OutputReference[] { + const pattern = /\$\{([^}]+)\}/g + let match + var result = [] + while ((match = pattern.exec(str)) !== null) { + const spl = split(match[1], '.', 2) + result.push(new OutputReference(spl[0], spl[1])) + } + return result + } + + /** + * Replaces occurrences of output references. + * Takes special care of preserving the type of the reference value if the entire string is a reference. + * + * @param str a string that may contain output references + * @param callback a callback for fetching reference values + * @returns a string with output references replaced by their values + */ + static replace(str: string, callback: (chapter_id: any, variable: any) => string): any { + let full_match = str.match(/^\$\{([^}]+)\}$/) + if (full_match) { + // the entire string is a reference, preserve the type of the returned value + const spl = split(full_match[1], '.', 2) + return callback(spl[0], spl[1]) + } else return str.replace(/\$\{([^}]+)\}/g, (_, k) => { + const spl = split(k, '.', 2) + return callback(spl[0], spl[1]) + }) + } +} diff --git a/tools/src/tester/StoryEvaluator.ts b/tools/src/tester/StoryEvaluator.ts index 839760dc..eb0bbf2f 100644 --- a/tools/src/tester/StoryEvaluator.ts +++ b/tools/src/tester/StoryEvaluator.ts @@ -8,7 +8,7 @@ */ import { ChapterRequest, Parameter, SupplementalChapter } from './types/story.types' -import { type StoryFile, type ChapterEvaluation, Result, type StoryEvaluation, OutputReference } from './types/eval.types' +import { type StoryFile, type ChapterEvaluation, Result, type StoryEvaluation } from './types/eval.types' import type ChapterEvaluator from './ChapterEvaluator' import { overall_result } from './helpers' import { StoryOutputs } from './StoryOutputs' @@ -17,6 +17,7 @@ import { ChapterOutput } from './ChapterOutput' import * as semver from '../_utils/semver' import _ from 'lodash' import { ParsedChapter, ParsedStory } from './types/parsed_story.types' +import { OutputReference } from './OutputReference' export default class StoryEvaluator { private readonly _chapter_evaluator: ChapterEvaluator diff --git a/tools/src/tester/types/eval.types.ts b/tools/src/tester/types/eval.types.ts index f29714af..0686a689 100644 --- a/tools/src/tester/types/eval.types.ts +++ b/tools/src/tester/types/eval.types.ts @@ -83,46 +83,3 @@ export enum Result { SKIPPED = 'SKIPPED', ERROR = 'ERROR', } - -export class OutputReference { - chapter_id: string - output_name: string - - private constructor(chapter_id: string, output_name: string) { - this.chapter_id = chapter_id - this.output_name = output_name - } - - static parse(str: string): OutputReference[] { - const pattern = /\$\{([^}]+)\}/g - let match - var result = [] - while ((match = pattern.exec(str)) !== null) { - const spl = this.#split(match[1], '.', 2) - result.push(new OutputReference(spl[0], spl[1])) - } - return result - } - - static replace(str: string, callback: (chapter_id: any, variable: any) => string): any { - // preserve type if 1 value is returned - let full_match = str.match(/^\$\{([^}]+)\}$/) - if (full_match) { - const spl = this.#split(full_match[1], '.', 2) - return callback(spl[0], spl[1]) - } else return str.replace(/\$\{([^}]+)\}/g, (_, k) => { - const spl = this.#split(k, '.', 2) - return callback(spl[0], spl[1]) - }); - } - - static #split(str: any, delim: string, count: number): string[] { - if (str === undefined) return [str] - if (count <= 0) return [str] - const parts = str.split(delim) - if (parts.length <= count) return parts - const result = parts.slice(0, count - 1) - result.push(parts.slice(count - 1).join(delim)) - return result - } -} diff --git a/tools/tests/helpers.test.ts b/tools/tests/helpers.test.ts index f91dde57..3e1f1547 100644 --- a/tools/tests/helpers.test.ts +++ b/tools/tests/helpers.test.ts @@ -8,7 +8,7 @@ */ import _ from 'lodash' -import { delete_matching_keys, find_refs, sort_array_by_keys, to_json, to_ndjson } from '../src/helpers' +import { delete_matching_keys, find_refs, sort_array_by_keys, to_json, to_ndjson, split } from '../src/helpers' describe('helpers', () => { describe('sort_array_by_keys', () => { @@ -195,4 +195,27 @@ describe('helpers', () => { })).toEqual(new Set(['#1', '#2', '#3', '#dup', '#/schemas/schema1', '#/schemas/schema2'])) }) }) + + describe('split', () => { + test('undefined', () => { + expect(split(undefined, '.')).toEqual([]) + }) + + test('one element', () => { + expect(split('str', '.')).toEqual(['str']) + expect(split('str', '.', -1)).toEqual(['str']) + expect(split('str', '.', 0)).toEqual(['str']) + expect(split('str', '.', 10)).toEqual(['str']) + }) + + test('multiple elements', () => { + expect(split('x.y.z', '.')).toEqual(['x', 'y', 'z']) + expect(split('x.y.z', '.', -1)).toEqual(['x', 'y', 'z']) + expect(split('x.y.z', '.', 0)).toEqual(['x', 'y', 'z']) + expect(split('x.y.z', '.', 1)).toEqual(['x.y.z']) + expect(split('x.y.z', '.', 2)).toEqual(['x', 'y.z']) + expect(split('x.y.z', '.', 3)).toEqual(['x', 'y', 'z']) + expect(split('x.y.z', '.', 4)).toEqual(['x', 'y', 'z']) + }) + }) }) diff --git a/tools/tests/tester/OutputReference.test.ts b/tools/tests/tester/OutputReference.test.ts new file mode 100644 index 00000000..c5efdd0e --- /dev/null +++ b/tools/tests/tester/OutputReference.test.ts @@ -0,0 +1,40 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { OutputReference } from "tester/OutputReference"; + +describe('OutputReference', () => { + let f = (id: any, k: any): string => `[${id}:${k}]` + + describe('parse', () => { + it('replaces', () => { + expect(OutputReference.parse('string')).toEqual([]) + expect(OutputReference.parse('${k.v}')).toEqual([new OutputReference('k', 'v')]) + expect(OutputReference.parse('${k.value}')).toEqual([new OutputReference('k', 'value')]) + expect(OutputReference.parse('${k.v.m}')).toEqual([new OutputReference('k', 'v.m')]) + expect(OutputReference.parse('A reference to ${k.v.m} and ${x} and ${x.y}.')).toEqual([ + new OutputReference('k', 'v.m'), + new OutputReference('x'), + new OutputReference('x', 'y') + ]) + }) + }) + + describe('replace', () => { + it('replaces', () => { + expect(OutputReference.replace('string', f)).toEqual('string') + expect(OutputReference.replace('${k.v}', f)).toEqual('[k:v]') + expect(OutputReference.replace('${k.value}', f)).toEqual('[k:value]') + expect(OutputReference.replace('${k.v.m}', f)).toEqual('[k:v.m]') + expect(OutputReference.replace('A reference to ${k.v.m} and ${x} and ${x.y}.', f)).toEqual( + 'A reference to [k:v.m] and [x:undefined] and [x:y].' + ) + }) + }) +}); diff --git a/tools/tests/types/eval.types.test.ts b/tools/tests/types/eval.types.test.ts deleted file mode 100644 index 76f7f3ac..00000000 --- a/tools/tests/types/eval.types.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -* Copyright OpenSearch Contributors -* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -*/ - -import { OutputReference } from "tester/types/eval.types"; - -describe('OutputReference', () => { - let f = (id: any, k: any): string => `[${id}:${k}]` - - describe('replace', () => { - it('replaces', () => { - expect(OutputReference.replace('string', f)).toEqual('string') - expect(OutputReference.replace('${k.v}', f)).toEqual('[k:v]') - expect(OutputReference.replace('${k.value}', f)).toEqual('[k:value]') - expect(OutputReference.replace('${k.v.m}', f)).toEqual('[k:v.m]') - }) - }) -});