Skip to content

Commit

Permalink
Add a payload parser to support JSON payloads
Browse files Browse the repository at this point in the history
This change enabled step definitions to declare a parsed payload
Given('…', (...args) => { … }, { parser: 'json' | 'json5' })

This will remove the need to JSON.parse in every step-def.
  • Loading branch information
micaelbergeron committed May 12, 2023
1 parent 38e5e79 commit f9f538d
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 30 deletions.
27 changes: 27 additions & 0 deletions features/misc/parser.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Feature: payload parser

Scenario: happy path
Then JSON representation of the payload is {"a":1,"b":2}
"""
{
"a": 1,
"b": 2
}
"""

Scenario: JSON5 support
Given variable A is 123
Then JSON representation of the payload is {"a":1,"b":2,"arrays":[123,"123"],"nested":{"a":1}}
"""
{
a: 1,
b: 2,
arrays: [
${A},
"${A}",
],
nested: {
"a": 1,
},
}
"""
51 changes: 35 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pickled-cucumber",
"version": "6.1.2",
"version": "6.2.0",
"description": "Cucumber test runner with several condiments",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"@cucumber/cucumber": "8.5.1",
"json5": "^2.2.3",
"node-fetch": "^2.2.0",
"ts-node": "^7.0.0"
},
Expand Down
61 changes: 48 additions & 13 deletions src/steps/constructor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import JSON5 from 'json5';
import BUILT_IN_ALIASES from '../aliases';
import { Aliases, Context } from '../types';
import { getDeep } from '../util';
import { Step, StepFn, StepKind, StepOptions } from './types';
import {
ParserFn,
ParserKind,
Step,
StepFn,
StepKind,
StepOptions,
} from './types';

const PARSER_MAP: Record<ParserKind, ParserFn> = {
json: JSON5.parse,
} as const;

const getDeepString = (ctx: Context, path: string): string => {
const v = getDeep(ctx, path);
Expand All @@ -27,16 +39,30 @@ const resolveRegExp = (aliases: Aliases, regexpString: string) =>
);

// Creates a proxy of fn that calls `expand` on every argument
const proxyFnFor = (getCtx: () => Context, fn: StepFn, argCount: number) => {
// tslint:disable-next-line prefer-array-literal
const args = new Array(argCount).fill(undefined).map((_, i) => `a${i}`);
const body = `{
const { fn, expand } = this;
const ex = expand(this.getCtx());
return fn(${args.map((a) => `ex(${a})`).join(',')});
}`;
// tslint:disable-next-line no-function-constructor-with-string-args
return new Function(...args, body).bind({ getCtx, expand, fn });
const proxyFnFor = (
getCtx: () => Context,
fn: StepFn,
argCount: number,
parserFn?: ParserFn,
) => {
// this will convert the variadic args to a fixed-length function
// which is used internally to run the actual step definition
const proxyFn = (...args: string[]) => {
// reverse the args list handle the `payload` arg
const ex = expand(getCtx());
const [errorFn, ...fnArgs] = args.reverse();
const [tail, ...head] = fnArgs.map(ex);
const parse = parserFn ? parserFn : (x: string) => x;

const proxyArgs = [...head.reverse(), parse(tail) as any, errorFn];

Check warning on line 57 in src/steps/constructor.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
return fn.call(null, ...proxyArgs.slice(0, argCount));
};

// we are using this internally to differenciate the function kind
// this makes the 'proxyFn' mimic a N-args function just fine
Object.defineProperty(proxyFn, 'length', { get: () => argCount });

return proxyFn;
};

// Generates a cucumber step, with some additional features:
Expand All @@ -56,15 +82,23 @@ const proxyFnFor = (getCtx: () => Context, fn: StepFn, argCount: number) => {
// version of the step that does not include the last argument. Also, when
// `optional` is a string, that string is appended to all other version of
// this step. Note that setting `optional` implies `inline`
//
// - setting `opt.parser` will automatically add a payload parser before sending
// the payload to your step definition handler; currently 'json' is available
// available
export default (aliases: Aliases, getCtx: () => Context) => (
kind: StepKind,
regexpString: string,
fn: StepFn,
opt: StepOptions = {},
): Step[] => {
// Generate a proxy of fn that calls `expand` on every argument
const proxyFn = proxyFnFor(getCtx, fn, fn.length);

const proxyFn = proxyFnFor(
getCtx,
fn,
fn.length,
opt.parser ? PARSER_MAP[opt.parser] : undefined,
);
const allAliases = { ...BUILT_IN_ALIASES, ...aliases };

const rawRegExp = resolveRegExp(allAliases, regexpString);
Expand All @@ -80,6 +114,7 @@ export default (aliases: Aliases, getCtx: () => Context) => (
fn,
fn.length - 1 - (suffix.match(/(\([^)]*\))/g) || []).length,
);

steps.push({
fn: proxyFnWithoutLastArg,
kind,
Expand Down
3 changes: 3 additions & 0 deletions src/steps/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export type StepFn = (...args: string[]) => void;
export type ParserFn = (raw: string) => unknown;
export type ParserKind = 'json';

export interface StepOptions {
inline?: boolean;
optional?: string | boolean;
parser?: ParserKind;
}

export type StepKind = 'Given' | 'Then' | 'When';
Expand Down
7 changes: 7 additions & 0 deletions src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ const fn: SetupFn = ({ getCtx, Given, onTearDown, setCtx, Then, When }) => {
assert.equal(getCtx(name), val),
);

// === Test parser ===================================================== //
Then(
'JSON representation of the payload is (.*)',
(repr, payload) => assert.equal(repr, JSON.stringify(payload)),
{ parser: 'json' },
);

// === Test output ======================================================== //
Given('step definition', (payload) => setCtx('steps-definition', payload));
Given('feature file is', (payload) =>
Expand Down

0 comments on commit f9f538d

Please sign in to comment.