Add support for pipes in templates #53
The goal is to be able to modify values of variables used in templates. It enables future functionality such as escaping, inlining etc. It adds support applying predefined pipes to variables. Pipes can be applied to variable substitution in with and parameter substitution expressions. They work in similar way to piping in Unix where each pipe applied to the compiled result of pipe before. It adds support for using pipes in `with` and parameter substitution expressions. It also refactors how their regex is build to reuse more of the logic by abstracting regex building into a new class. Finally, it separates and extends documentation for templating.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { PipeFactory } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory';
|
||||
import { PipeStub } from '@tests/unit/stubs/PipeStub';
|
||||
|
||||
describe('PipeFactory', () => {
|
||||
describe('ctor', () => {
|
||||
it('throws when instances with same name is registered', () => {
|
||||
// arrange
|
||||
const duplicateName = 'duplicateName';
|
||||
const expectedError = `Pipe name must be unique: "${duplicateName}"`;
|
||||
const pipes = [
|
||||
new PipeStub().withName(duplicateName),
|
||||
new PipeStub().withName('uniqueName'),
|
||||
new PipeStub().withName(duplicateName),
|
||||
];
|
||||
// act
|
||||
const act = () => new PipeFactory(pipes);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when a pipe is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined pipe in list';
|
||||
const pipes = [ new PipeStub(), undefined ];
|
||||
// act
|
||||
const act = () => new PipeFactory(pipes);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when name is invalid', () => {
|
||||
// act
|
||||
const act = (invalidName: string) => new PipeFactory([ new PipeStub().withName(invalidName) ]);
|
||||
// assert
|
||||
testPipeNameValidation(act);
|
||||
});
|
||||
});
|
||||
describe('get', () => {
|
||||
describe('throws when name is invalid', () => {
|
||||
// arrange
|
||||
const sut = new PipeFactory();
|
||||
// act
|
||||
const act = (invalidName: string) => sut.get(invalidName);
|
||||
// assert
|
||||
testPipeNameValidation(act);
|
||||
});
|
||||
it('gets registered instance when it exists', () => {
|
||||
// arrange
|
||||
const expected = new PipeStub().withName('expectedName');
|
||||
const pipes = [ expected, new PipeStub().withName('instanceToConfuse') ];
|
||||
const sut = new PipeFactory(pipes);
|
||||
// act
|
||||
const actual = sut.get(expected.name);
|
||||
// expect
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
it('throws when instance does not exist', () => {
|
||||
// arrange
|
||||
const missingName = 'missingName';
|
||||
const expectedError = `Unknown pipe: "${missingName}"`;
|
||||
const pipes = [ ];
|
||||
const sut = new PipeFactory(pipes);
|
||||
// act
|
||||
const act = () => sut.get(missingName);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPipeNameValidation(testRunner: (invalidName: string) => void) {
|
||||
const testCases = [
|
||||
{
|
||||
exceptionBuilder: () => 'empty pipe name',
|
||||
values: [ null, undefined , ''],
|
||||
},
|
||||
{
|
||||
exceptionBuilder: (name: string) => `Pipe name should be camelCase: "${name}"`,
|
||||
values: [
|
||||
'PascalCase',
|
||||
'snake-case',
|
||||
'includesNumb3rs',
|
||||
'includes Whitespace',
|
||||
'noSpec\'ial',
|
||||
],
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
for (const invalidName of testCase.values) {
|
||||
it(`invalid name (${printValue(invalidName)}) throws`, () => {
|
||||
// arrange
|
||||
const expectedError = testCase.exceptionBuilder(invalidName);
|
||||
// act
|
||||
const act = () => testRunner(invalidName);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printValue(value: string) {
|
||||
switch (value) {
|
||||
case undefined:
|
||||
return 'undefined';
|
||||
case null:
|
||||
return 'null';
|
||||
case '':
|
||||
return 'empty';
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { PipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipelineCompiler';
|
||||
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
|
||||
import { IPipeFactory } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory';
|
||||
import { PipeStub } from '@tests/unit/stubs/PipeStub';
|
||||
import { PipeFactoryStub } from '@tests/unit/stubs/PipeFactoryStub';
|
||||
|
||||
describe('PipelineCompiler', () => {
|
||||
describe('compile', () => {
|
||||
describe('throws for invalid arguments', () => {
|
||||
interface ITestCase {
|
||||
name: string;
|
||||
act: (test: PipelineTestRunner) => PipelineTestRunner;
|
||||
expectedError: string;
|
||||
}
|
||||
const testCases: ITestCase[] = [
|
||||
{
|
||||
name: '"value" is empty',
|
||||
act: (test) => test.withValue(''),
|
||||
expectedError: 'undefined value',
|
||||
},
|
||||
{
|
||||
name: '"value" is undefined',
|
||||
act: (test) => test.withValue(undefined),
|
||||
expectedError: 'undefined value',
|
||||
},
|
||||
{
|
||||
name: '"pipeline" is empty',
|
||||
act: (test) => test.withPipeline(''),
|
||||
expectedError: 'undefined pipeline',
|
||||
},
|
||||
{
|
||||
name: '"pipeline" is undefined',
|
||||
act: (test) => test.withPipeline(undefined),
|
||||
expectedError: 'undefined pipeline',
|
||||
},
|
||||
{
|
||||
name: '"pipeline" does not start with pipe',
|
||||
act: (test) => test.withPipeline('pipeline |'),
|
||||
expectedError: 'pipeline does not start with pipe',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
// act
|
||||
const runner = new PipelineTestRunner();
|
||||
testCase.act(runner);
|
||||
const act = () => runner.compile();
|
||||
// assert
|
||||
expect(act).to.throw(testCase.expectedError);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('compiles pipeline as expected', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'compiles single pipe as expected',
|
||||
pipes: [
|
||||
new PipeStub().withName('doublePrint').withApplier((value) => `${value}-${value}`),
|
||||
],
|
||||
pipeline: '| doublePrint',
|
||||
value: 'value',
|
||||
expected: 'value-value',
|
||||
},
|
||||
{
|
||||
name: 'compiles multiple pipes as expected',
|
||||
pipes: [
|
||||
new PipeStub().withName('prependLetterA').withApplier((value) => `A-${value}`),
|
||||
new PipeStub().withName('prependLetterB').withApplier((value) => `B-${value}`),
|
||||
],
|
||||
pipeline: '| prependLetterA | prependLetterB',
|
||||
value: 'value',
|
||||
expected: 'B-A-value',
|
||||
},
|
||||
{
|
||||
name: 'compiles with relaxed whitespace placing',
|
||||
pipes: [
|
||||
new PipeStub().withName('appendNumberOne').withApplier((value) => `${value}1`),
|
||||
new PipeStub().withName('appendNumberTwo').withApplier((value) => `${value}2`),
|
||||
new PipeStub().withName('appendNumberThree').withApplier((value) => `${value}3`),
|
||||
],
|
||||
pipeline: ' | appendNumberOne|appendNumberTwo| appendNumberThree',
|
||||
value: 'value',
|
||||
expected: 'value123',
|
||||
},
|
||||
{
|
||||
name: 'can reuse same pipe',
|
||||
pipes: [
|
||||
new PipeStub().withName('removeFirstChar').withApplier((value) => `${value.slice(1)}`),
|
||||
],
|
||||
pipeline: ' | removeFirstChar | removeFirstChar | removeFirstChar',
|
||||
value: 'value',
|
||||
expected: 'ue',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
// arrange
|
||||
const runner =
|
||||
new PipelineTestRunner()
|
||||
.withValue(testCase.value)
|
||||
.withPipeline(testCase.pipeline)
|
||||
.withFactory(new PipeFactoryStub().withPipes(testCase.pipes));
|
||||
// act
|
||||
const actual = runner.compile();
|
||||
// expect
|
||||
expect(actual).to.equal(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class PipelineTestRunner implements IPipelineCompiler {
|
||||
private value: string = 'non-empty-value';
|
||||
private pipeline: string = '| validPipeline';
|
||||
private factory: IPipeFactory = new PipeFactoryStub();
|
||||
|
||||
public withValue(value: string) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
public withPipeline(pipeline: string) {
|
||||
this.pipeline = pipeline;
|
||||
return this;
|
||||
}
|
||||
public withFactory(factory: IPipeFactory) {
|
||||
this.factory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public compile(): string {
|
||||
const sut = new PipelineCompiler(this.factory);
|
||||
return sut.compile(this.value, this.pipeline);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user