From ced9df44b6763dc075586b7335dd3dc0b891cbbb Mon Sep 17 00:00:00 2001 From: BugDiver Date: Sun, 16 Jun 2024 14:52:25 +0530 Subject: [PATCH] Enhance step parameter tpyes * Parse primitive paramters * Support custom parameter parser Signed-off-by: BugDiver --- .vscode/settings.json | 11 ++++ biome.jsonc | 1 + docs/index.md | 57 ++++++++++++++++++ e2e/specs/example.spec | 4 ++ e2e/src/Person.ts | 12 ++++ e2e/tests/PersonParameterParser.ts | 14 +++++ e2e/tests/implementation.ts | 10 +++- gauge-ts/src/RunnerServer.ts | 9 ++- gauge-ts/src/index.ts | 4 ++ gauge-ts/src/loaders/ImplLoader.ts | 18 +++++- .../src/processors/StepExecutionProcessor.ts | 26 +++------ .../src/processors/params/ParameterParser.ts | 6 ++ .../params/ParameterParsingChain.ts | 26 +++++++++ .../src/processors/params/PrimitiveParser.ts | 37 ++++++++++++ .../processors/params/TableParameterParser.ts | 16 +++++ gauge-ts/src/utils/Util.ts | 8 +++ gauge-ts/tests/loaders/ImplLoaderTests.ts | 24 +++++++- .../processors/StepExecutionProcessorTests.ts | 8 ++- .../params/ParamterParsingChainTests.ts | 58 +++++++++++++++++++ package-lock.json | 2 +- 20 files changed, 323 insertions(+), 28 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 e2e/src/Person.ts create mode 100644 e2e/tests/PersonParameterParser.ts create mode 100644 gauge-ts/src/processors/params/ParameterParser.ts create mode 100644 gauge-ts/src/processors/params/ParameterParsingChain.ts create mode 100644 gauge-ts/src/processors/params/PrimitiveParser.ts create mode 100644 gauge-ts/src/processors/params/TableParameterParser.ts create mode 100644 gauge-ts/tests/processors/params/ParamterParsingChainTests.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..642c8cf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.associations": { + "*.spec": "gauge", + "*.cpt": "gauge" + }, + "files.autoSave": "afterDelay", + "files.autoSaveDelay": 500, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + } +} \ No newline at end of file diff --git a/biome.jsonc b/biome.jsonc index 05042b3..a74b787 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -2,6 +2,7 @@ "files": { "ignore": [ "dist/**", + "coverage/**", "node_modules", "src/gen", ".vscode", diff --git a/docs/index.md b/docs/index.md index a209686..a72e9f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -309,6 +309,63 @@ String elementId = scenarioStore.get("element-id") as string; ``` +### Custom Paramter Parsers + +* By default gauge-ts tries to convert the spec parameter to primitives (number, boolean), for table parameters gauge-ts will convert paramters +to `Table` type. gauge-ts also provide a way to customize the paramter parsing. +If you need to have custom paramters in your step implementations create a paramter parser which should implements methods from `ParamterParser` interface. + +Step definition: +```md + +* step with a person parameter type "{\"name\": \"John\", \"age\": 40 }" + +``` + +Step Implementation: +```javascript +import { Step } from 'gauge-ts'; + +export class Person { + name: string; + age: number; + constructor(name: string, age: number) { + this.name = name; + this.age = age; + } + + public isAdult(): boolean { + return this.age >= 18; + } +} + +export default class Implementation { + @Step + public async isAdult(person: Person)y { + assert.ok(person.isAdult()) + } +} + +``` +Cusotm ParameterParser: +```javascript + +import { Parameter, ParameterParser } from 'gauge-ts'; +import { Person } from '@lib/Person'; +export default class PersonParameterParser implements ParameterParser { + + public canParse(paramter: Parameter): boolean { + return paramter.getValue().startsWith("{") && paramter.getValue().endsWith("}"); + } + + public parse(parameter: Parameter): Object { + const person = JSON.parse(parameter.getValue()); + return new Person(person.name, person.age); + } +} + +``` + ### Custom Screenshots * By default gauge captures the display screen on failure if this feature has been enabled. diff --git a/e2e/specs/example.spec b/e2e/specs/example.spec index dd2ccc4..724ac13 100644 --- a/e2e/specs/example.spec +++ b/e2e/specs/example.spec @@ -30,3 +30,7 @@ Here's a step that takes a table |Snap |1 | |GoCD |1 | |Rhythm|0 | + +## Custom Parameters in steps + +* This step uses a custom parameter of type Person and value "{\"name\":\"John\",\"age\":30}" diff --git a/e2e/src/Person.ts b/e2e/src/Person.ts new file mode 100644 index 0000000..ca44992 --- /dev/null +++ b/e2e/src/Person.ts @@ -0,0 +1,12 @@ +export class Person { + name: string; + age: number; + constructor(name: string, age: number) { + this.name = name; + this.age = age; + } + + public isAdult(): boolean { + return this.age >= 18; + } +} diff --git a/e2e/tests/PersonParameterParser.ts b/e2e/tests/PersonParameterParser.ts new file mode 100644 index 0000000..b8f221f --- /dev/null +++ b/e2e/tests/PersonParameterParser.ts @@ -0,0 +1,14 @@ +import { Person } from "@lib/Person"; +import type { Parameter, ParameterParser } from "gauge-ts"; +export default class PersonParameterParser implements ParameterParser { + public canParse(paramter: Parameter): boolean { + return ( + paramter.getValue().startsWith("{") && paramter.getValue().endsWith("}") + ); + } + + public parse(parameter: Parameter): Person { + const person = JSON.parse(parameter.getValue()); + return new Person(person.name, person.age); + } +} diff --git a/e2e/tests/implementation.ts b/e2e/tests/implementation.ts index 139debe..481022a 100644 --- a/e2e/tests/implementation.ts +++ b/e2e/tests/implementation.ts @@ -2,7 +2,9 @@ import * as assert from "node:assert"; import VowelCounter from "@lib/VowelCounter"; import { DataStoreFactory, Step, type Table } from "gauge-ts"; -class Implementation { +import type { Person } from "@lib/Person"; + +export default class Implementation { static vowelsCount = (word: string): number => { const counter = DataStoreFactory.getSpecDataStore().get( "counter", @@ -31,4 +33,10 @@ class Implementation { assert.equal(Implementation.vowelsCount(word), Number.parseInt(count)); } } + @Step("This step uses a custom parameter of type Person and value ") + public async validatePerson(person: Person) { + assert.equal(person.name, "John"); + assert.equal(person.age, 30); + assert.ok(person.isAdult()); + } } diff --git a/gauge-ts/src/RunnerServer.ts b/gauge-ts/src/RunnerServer.ts index e5fce72..19584ac 100644 --- a/gauge-ts/src/RunnerServer.ts +++ b/gauge-ts/src/RunnerServer.ts @@ -66,6 +66,7 @@ import { StepNameProcessor } from "./processors/StepNameProcessor"; import { StepPositionsProcessor } from "./processors/StepPositionsProcessor"; import { StubImplementationCodeProcessor } from "./processors/StubImplementationCodeProcessor"; import { ValidationProcessor } from "./processors/ValidationProcessor"; +import { ParameterParsingChain } from "./processors/params/ParameterParsingChain"; import { Util } from "./utils/Util"; type RpcError = { @@ -92,8 +93,10 @@ export default class RunnerServer implements IRunnerServer { private static stepPositionsProcessor: StepPositionsProcessor; private static stubImplementationCodeProcessor: StubImplementationCodeProcessor; private static validationProcessor: ValidationProcessor; + private static paramterParsingChain: ParameterParsingChain; constructor(loader: StaticLoader) { + RunnerServer.paramterParsingChain = new ParameterParsingChain(); loader.loadImplementations(); RunnerServer.cacheFileProcessor = new CacheFileProcessor(loader); RunnerServer.executionEndingProcessor = new ExecutionEndingProcessor(); @@ -109,7 +112,9 @@ export default class RunnerServer implements IRunnerServer { new SpecExecutionStartingProcessor(); RunnerServer.stepExecutionEndingProcessor = new StepExecutionEndingProcessor(); - RunnerServer.stepExecutionProcessor = new StepExecutionProcessor(); + RunnerServer.stepExecutionProcessor = new StepExecutionProcessor( + RunnerServer.paramterParsingChain, + ); RunnerServer.stepExecutionStartingProcessor = new StepExecutionStartingProcessor(); RunnerServer.stepNameProcessor = new StepNameProcessor(); @@ -141,7 +146,7 @@ export default class RunnerServer implements IRunnerServer { ): void { try { DataStoreFactory.getSuiteDataStore().clear(); - const loader = new ImplLoader(); + const loader = new ImplLoader(RunnerServer.paramterParsingChain); loader .loadImplementations() diff --git a/gauge-ts/src/index.ts b/gauge-ts/src/index.ts index 90309ef..8f43d81 100644 --- a/gauge-ts/src/index.ts +++ b/gauge-ts/src/index.ts @@ -1,3 +1,5 @@ +import { Parameter } from "./gen/spec_pb"; +import { ParameterParser } from "./processors/params/ParameterParser"; import { Gauge } from "./public/Gauge"; import { Operator } from "./public/Operator"; import { Table } from "./public/Table"; @@ -38,6 +40,8 @@ export { AfterSpec, AfterSuite, CustomScreenshotWriter, + ParameterParser, + Parameter, ExecutionContext, Specification, Scenario, diff --git a/gauge-ts/src/loaders/ImplLoader.ts b/gauge-ts/src/loaders/ImplLoader.ts index 1ec4fe0..2ff5927 100644 --- a/gauge-ts/src/loaders/ImplLoader.ts +++ b/gauge-ts/src/loaders/ImplLoader.ts @@ -1,5 +1,7 @@ import hookRegistry from "../models/HookRegistry"; import registry from "../models/StepRegistry"; +import type { ParameterParser } from "../processors/params/ParameterParser"; +import type { ParameterParsingChain } from "../processors/params/ParameterParsingChain"; import { Util } from "../utils/Util"; type ConstructorType = new () => Record; @@ -9,6 +11,12 @@ type ModuleType = { }; export class ImplLoader { + private paramterParsignChain: ParameterParsingChain; + + constructor(paramterParsignChain: ParameterParsingChain) { + this.paramterParsignChain = paramterParsignChain; + } + public async loadImplementations(): Promise { registry.clear(); hookRegistry.clear(); @@ -16,11 +24,15 @@ export class ImplLoader { try { process.env.STEP_FILE_PATH = file; const c = (await Util.importFile(file)) as ModuleType; - if (c.default && c.default.length === 0) { const instance = new c.default(); - - ImplLoader.updateRegistry(file, instance); + if (Util.isCustomParameterParser(instance)) { + this.paramterParsignChain.addCustomParser( + instance as ParameterParser, + ); + } else { + ImplLoader.updateRegistry(file, instance); + } } } catch (error) { const err = error as Error; diff --git a/gauge-ts/src/processors/StepExecutionProcessor.ts b/gauge-ts/src/processors/StepExecutionProcessor.ts index f099b14..78429d9 100644 --- a/gauge-ts/src/processors/StepExecutionProcessor.ts +++ b/gauge-ts/src/processors/StepExecutionProcessor.ts @@ -2,20 +2,21 @@ import type { ExecuteStepRequest, ExecutionStatusResponse, } from "../gen/messages_pb"; -import { - Parameter, - ProtoExecutionResult, - type ProtoTable, -} from "../gen/spec_pb"; +import { ProtoExecutionResult } from "../gen/spec_pb"; import registry from "../models/StepRegistry"; -import { Table } from "../public/Table"; import { Screenshot } from "../screenshot/Screenshot"; import { MessageStore } from "../stores/MessageStore"; import { ScreenshotStore } from "../stores/ScreenshotStore"; import type { CommonFunction } from "../utils/Util"; import { ExecutionProcessor } from "./ExecutionProcessor"; +import type { ParameterParsingChain } from "./params/ParameterParsingChain"; export class StepExecutionProcessor extends ExecutionProcessor { + private parsingChain: ParameterParsingChain; + constructor(parameterParsingChain: ParameterParsingChain) { + super(); + this.parsingChain = parameterParsingChain; + } public async process( req: ExecuteStepRequest, ): Promise { @@ -37,11 +38,7 @@ export class StepExecutionProcessor extends ExecutionProcessor { result.setFailed(false); const mi = registry.get(req.getParsedsteptext()); - const params = req.getParametersList().map((item) => { - return this.isTable(item) - ? Table.from(item.getTable() as ProtoTable) - : item.getValue(); - }); + const params = req.getParametersList().map(this.parsingChain.parse); const method = mi.getMethod() as CommonFunction; @@ -81,13 +78,6 @@ export class StepExecutionProcessor extends ExecutionProcessor { return result; } - private isTable(item: Parameter): boolean { - return ( - item.getParametertype() === Parameter.ParameterType.TABLE || - item.getParametertype() === Parameter.ParameterType.SPECIAL_TABLE - ); - } - private executionError(message: string): ExecutionStatusResponse { const result = new ProtoExecutionResult(); diff --git a/gauge-ts/src/processors/params/ParameterParser.ts b/gauge-ts/src/processors/params/ParameterParser.ts new file mode 100644 index 0000000..83e3484 --- /dev/null +++ b/gauge-ts/src/processors/params/ParameterParser.ts @@ -0,0 +1,6 @@ +import type { Parameter } from "../../gen/spec_pb"; + +export interface ParameterParser { + canParse(paramter: Parameter): boolean; + parse(parameter: Parameter): unknown; +} diff --git a/gauge-ts/src/processors/params/ParameterParsingChain.ts b/gauge-ts/src/processors/params/ParameterParsingChain.ts new file mode 100644 index 0000000..f9ba917 --- /dev/null +++ b/gauge-ts/src/processors/params/ParameterParsingChain.ts @@ -0,0 +1,26 @@ +import type { Parameter } from "../../gen/spec_pb"; +import type { ParameterParser } from "./ParameterParser"; +import { PrimitiveParser } from "./PrimitiveParser"; +import { TableParameterParser } from "./TableParameterParser"; + +export class ParameterParsingChain { + private chain: Array = []; + + public constructor() { + this.chain.push(new TableParameterParser()); + this.chain.push(new PrimitiveParser()); + } + + public parse(parameter: Parameter): unknown { + for (const parser of this.chain) { + if (parser.canParse(parameter)) { + return parser.parse(parameter); + } + } + return parameter.getValue(); + } + + public addCustomParser(parser: ParameterParser): void { + this.chain.unshift(parser); + } +} diff --git a/gauge-ts/src/processors/params/PrimitiveParser.ts b/gauge-ts/src/processors/params/PrimitiveParser.ts new file mode 100644 index 0000000..958a176 --- /dev/null +++ b/gauge-ts/src/processors/params/PrimitiveParser.ts @@ -0,0 +1,37 @@ +import type { Parameter } from "../../gen/spec_pb"; +import type { ParameterParser } from "./ParameterParser"; + +type ConvertFunction = (value: string) => unknown | undefined; + +export class PrimitiveParser implements ParameterParser { + public readonly converters: ConvertFunction[] = []; + + constructor() { + this.converters.push(this.convertToNumber); + this.converters.push(this.convertToBoolean); + } + + public canParse(parameter: Parameter): boolean { + return true; + } + + public parse(parameter: Parameter): unknown { + const paramValue = parameter.getValue(); + for (const converter of this.converters) { + const v = converter(paramValue); + if (v !== undefined) { + return v; + } + } + return paramValue; + } + + private convertToNumber(value: string): number | undefined { + const num = Number(value); + return Number.isNaN(num) ? undefined : num; + } + + private convertToBoolean(value: string): boolean | undefined { + return value === "true" || value === "false" ? value === "true" : undefined; + } +} diff --git a/gauge-ts/src/processors/params/TableParameterParser.ts b/gauge-ts/src/processors/params/TableParameterParser.ts new file mode 100644 index 0000000..e0c5d1a --- /dev/null +++ b/gauge-ts/src/processors/params/TableParameterParser.ts @@ -0,0 +1,16 @@ +import { Parameter, type ProtoTable } from "../../gen/spec_pb"; +import { Table } from "../../public/Table"; +import type { ParameterParser } from "./ParameterParser"; + +export class TableParameterParser implements ParameterParser { + public canParse(parameter: Parameter): boolean { + return ( + parameter.getParametertype() === Parameter.ParameterType.TABLE || + parameter.getParametertype() === Parameter.ParameterType.SPECIAL_TABLE + ); + } + + public parse(parameter: Parameter): Table { + return Table.from(parameter.getTable() as ProtoTable); + } +} diff --git a/gauge-ts/src/utils/Util.ts b/gauge-ts/src/utils/Util.ts index 5ec010d..b56a67d 100644 --- a/gauge-ts/src/utils/Util.ts +++ b/gauge-ts/src/utils/Util.ts @@ -9,6 +9,7 @@ import { import { extname, join } from "node:path"; import { Extension } from "typescript"; import { v4 } from "uuid"; +import type { ParameterParser } from "../processors/params/ParameterParser"; export type CommonFunction = (...args: unknown[]) => T; export type CommonAsyncFunction = ( @@ -41,6 +42,13 @@ export class Util { return spawnSync(command, args); } + public static isCustomParameterParser( + // biome-ignore lint/suspicious/noExplicitAny: + object: any, + ): object is ParameterParser { + return object.canParse && object.parse !== undefined; + } + public static getListOfFiles() { return Util.getImplDirs().reduce((files: Array, dir) => { if (!existsSync(dir)) { diff --git a/gauge-ts/tests/loaders/ImplLoaderTests.ts b/gauge-ts/tests/loaders/ImplLoaderTests.ts index a97b497..8f55acb 100644 --- a/gauge-ts/tests/loaders/ImplLoaderTests.ts +++ b/gauge-ts/tests/loaders/ImplLoaderTests.ts @@ -1,11 +1,19 @@ import { ImplLoader } from "../../src/loaders/ImplLoader"; +import type { ParameterParsingChain } from "../../src/processors/params/ParameterParsingChain"; import { Util } from "../../src/utils/Util"; +jest.mock("../../src/processors/params/ParameterParsingChain"); + describe("ImplLoader", () => { let loader: ImplLoader; - + let chain: ParameterParsingChain; beforeEach(() => { - loader = new ImplLoader(); + chain = { + addCustomParser: jest.fn(), + canParse: jest.fn(), + parse: jest.fn(), + } as unknown as ParameterParsingChain; + loader = new ImplLoader(chain); }); afterEach(() => { @@ -44,5 +52,17 @@ describe("ImplLoader", () => { await loader.loadImplementations(); expect(exception).toContain("failed to import"); }); + + it("shold load custom parameter parser", async () => { + Util.getListOfFiles = jest.fn().mockReturnValue(["CustomParser.ts"]); + Util.importFile = jest.fn().mockReturnValue({ + default: function () { + this.canParse = jest.fn().mockReturnValue(true); + this.parse = jest.fn().mockReturnValue("parsed"); + }, + }); + await loader.loadImplementations(); + expect(chain.addCustomParser).toHaveBeenCalledWith(expect.any(Object)); + }); }); }); diff --git a/gauge-ts/tests/processors/StepExecutionProcessorTests.ts b/gauge-ts/tests/processors/StepExecutionProcessorTests.ts index 21bb708..68c13a7 100644 --- a/gauge-ts/tests/processors/StepExecutionProcessorTests.ts +++ b/gauge-ts/tests/processors/StepExecutionProcessorTests.ts @@ -5,6 +5,7 @@ import { Parameter, ProtoTable, ProtoTableRow } from "../../src/gen/spec_pb"; import registry from "../../src/models/StepRegistry"; import { StepRegistryEntry } from "../../src/models/StepRegistryEntry"; import { StepExecutionProcessor } from "../../src/processors/StepExecutionProcessor"; +import type { ParameterParsingChain } from "../../src/processors/params/ParameterParsingChain"; import { Screenshot } from "../../src/screenshot/Screenshot"; describe("StepExecutionProcessor", () => { @@ -13,7 +14,12 @@ describe("StepExecutionProcessor", () => { beforeEach(() => { jest.clearAllMocks(); Screenshot.capture = jest.fn(); - processor = new StepExecutionProcessor(); + const chain = { + parse: jest.fn(), + canParse: jest.fn(), + addCustomParser: jest.fn(), + } as unknown as ParameterParsingChain; + processor = new StepExecutionProcessor(chain); }); describe(".process", () => { diff --git a/gauge-ts/tests/processors/params/ParamterParsingChainTests.ts b/gauge-ts/tests/processors/params/ParamterParsingChainTests.ts new file mode 100644 index 0000000..4ec685d --- /dev/null +++ b/gauge-ts/tests/processors/params/ParamterParsingChainTests.ts @@ -0,0 +1,58 @@ +import { Parameter, ProtoTable, ProtoTableRow } from "../../../src/gen/spec_pb"; +import { ParameterParsingChain } from "../../../src/processors/params/ParameterParsingChain"; + +describe("ParameterParsingChain", () => { + let parameterParsingChain: ParameterParsingChain; + beforeEach(() => { + jest.resetModules(); + parameterParsingChain = new ParameterParsingChain(); + }); + + describe(".parse", () => { + it("should return table when parameter is table", () => { + const table = new Parameter(); + table.setParametertype(Parameter.ParameterType.TABLE); + const protoTable = new ProtoTable(); + const headers = new ProtoTableRow(); + headers.setCellsList(["foo", "bar"]); + protoTable.setHeaders(headers); + table.setTable(protoTable); + expect(parameterParsingChain.parse(table)).toBeDefined(); + }); + + it("should return number when parameter is number", () => { + const number = new Parameter(); + number.setParametertype(Parameter.ParameterType.STATIC); + number.setValue("1"); + expect(parameterParsingChain.parse(number)).toBe(1); + }); + + it("should return boolean when parameter is boolean", () => { + const bool = new Parameter(); + bool.setParametertype(Parameter.ParameterType.STATIC); + bool.setValue("true"); + expect(parameterParsingChain.parse(bool)).toBe(true); + }); + + it("should return string when parameter is string", () => { + const str = new Parameter(); + str.setParametertype(Parameter.ParameterType.STATIC); + str.setValue("foo"); + expect(parameterParsingChain.parse(str)).toBe("foo"); + }); + }); + + describe(".addCustomParser", () => { + it("should add custom parser to the chain", () => { + const customParser = { + canParse: jest.fn().mockReturnValue(true), + parse: jest.fn().mockReturnValue("custom"), + }; + parameterParsingChain.addCustomParser(customParser); + const param = new Parameter(); + param.setParametertype(Parameter.ParameterType.STATIC); + param.setValue("foo"); + expect(parameterParsingChain.parse(param)).toBe("custom"); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 0d937dd..6e6f524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ } }, "gauge-ts": { - "version": "0.3.0", + "version": "0.3.2", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.10.9",