Commit 94a56634 authored by David Sveningsson's avatar David Sveningsson

feat(transform): add version number to API

parent 6c304223
Pipeline #96408298 passed with stages
in 9 minutes and 30 seconds
......@@ -12,12 +12,16 @@ Each transformer must implement the following API:
```typescript
import { Transformer, TransformContext } from "html-validate";
module.exports = function(
this: TransformContext,
source: Source
): Iterable<Source> {
/* implementation */
function myTransform(this: TransformContext, source: Source): Iterable<Source> {
/* ... */
};
}
/* api version declaration */
myTransform.api = 1;
/* export */
module.exports = myTransform as Transfomer;
```
### `TransformContext`
......
......@@ -349,6 +349,17 @@ describe("config", () => {
`);
});
it("should throw error if transformer uses obsolete API", () => {
const config = Config.fromObject({
transform: {
"^.*\\.foo$": "mock-transform-obsolete",
},
});
expect(() => config.init()).toThrow(
/Failed to load transformer "mock-transform-obsolete": Transformer uses API version 0 but only version \d+ is supported/
);
});
it("should return original source if no transformer is found", () => {
const config = Config.fromObject({
transform: {
......
......@@ -6,7 +6,7 @@ import { NestedError } from "../error";
import { MetaTable } from "../meta";
import { MetaDataTable } from "../meta/element";
import { Plugin } from "../plugin";
import { TransformContext, Transformer } from "../transform";
import { TransformContext, Transformer, TRANSFORMER_API } from "../transform";
import { ConfigData, TransformMap } from "./config-data";
import defaultConfig from "./default";
import { ConfigError } from "./error";
......@@ -381,11 +381,21 @@ export class Config {
private precompileTransformers(transform: TransformMap): TransformerEntry[] {
return Object.entries(transform).map(([pattern, name]) => {
try {
const fn = this.getTransformFunction(name);
const version = (fn as any).api || 0;
/* check if transformer version is supported */
if (version !== TRANSFORMER_API.VERSION) {
throw new ConfigError(
`Transformer uses API version ${version} but only version ${TRANSFORMER_API.VERSION} is supported`
);
}
return {
// eslint-disable-next-line security/detect-non-literal-regexp
pattern: new RegExp(pattern),
fn: this.getTransformFunction(name),
fn,
};
} catch (err) {
if (err instanceof ConfigError) {
......
......@@ -4,7 +4,7 @@ import { Engine } from "../engine";
import { EventHandler } from "../event";
import { Parser } from "../parser";
import { Rule } from "../rule";
import { Transformer } from "../transform";
import { Transformer, TRANSFORMER_API } from "../transform";
import { Plugin } from "./plugin";
let mockPlugin: Plugin;
......@@ -227,7 +227,7 @@ describe("Plugin", () => {
describe("transform", () => {
it("should support exposing unnamed transform", () => {
expect.assertions(1);
mockPlugin.transformer = function transform(source: Source): Source[] {
function transform(source: Source): Source[] {
return [
{
data: "transformed from unnamed transformer",
......@@ -237,7 +237,9 @@ describe("Plugin", () => {
originalData: source.data,
},
];
} as Transformer;
}
transform.api = TRANSFORMER_API.VERSION;
mockPlugin.transformer = transform as Transformer;
config = Config.fromObject({
plugins: ["mock-plugin"],
transform: {
......@@ -266,18 +268,20 @@ describe("Plugin", () => {
it("should support exposing named transform", () => {
expect.assertions(1);
function transform(source: Source): Source[] {
return [
{
data: "transformed from named transformer",
filename: source.filename,
line: source.line,
column: source.column,
originalData: source.data,
},
];
}
transform.api = TRANSFORMER_API.VERSION;
mockPlugin.transformer = {
foobar: function transform(source: Source): Source[] {
return [
{
data: "transformed from named transformer",
filename: source.filename,
line: source.line,
column: source.column,
originalData: source.data,
},
];
} as Transformer,
foobar: transform as Transformer,
};
config = Config.fromObject({
plugins: ["mock-plugin"],
......
......@@ -3,11 +3,12 @@ import { Source } from "./context";
import { DynamicValue } from "./dom";
import HtmlValidate from "./htmlvalidate";
import { AttributeData } from "./parser";
import { Transformer, TRANSFORMER_API } from "./transform";
jest.mock(
"mock-transformer",
() => {
return function transformer(source: Source) {
function transformer(source: Source): Iterable<Source> {
source.hooks = {
*processAttribute(attr: AttributeData) {
yield attr;
......@@ -22,7 +23,9 @@ jest.mock(
},
};
return [source];
};
}
transformer.api = TRANSFORMER_API.VERSION;
return transformer as Transformer;
},
{ virtual: true }
);
......
import { Source } from "../../context";
import { Transformer, TransformContext } from "..";
import { Transformer, TransformContext, TRANSFORMER_API } from "..";
/**
* Mock transformer chaining to a .foo file transformer.
*/
module.exports = function* mockTransformChainFoo(
function* mockTransformChainFoo(
this: TransformContext,
source: Source
) {
): Iterable<Source> {
yield* this.chain(
{
data: `data from mock-transform-chain-foo (was: ${source.data})`,
......@@ -18,4 +18,9 @@ module.exports = function* mockTransformChainFoo(
},
`${source.filename}.foo`
);
} as Transformer;
}
/* mocks are always written against current version */
mockTransformChainFoo.api = TRANSFORMER_API.VERSION;
module.exports = mockTransformChainFoo as Transformer;
import { Source } from "../../context";
import { Transformer } from "..";
import { Transformer, TRANSFORMER_API } from "..";
/**
* Mock transformer always failing with an exception
*/
module.exports = function mockTransformError(): Iterable<Source> {
function mockTransformError(): Iterable<Source> {
throw new Error("Failed to frobnicate a baz");
} as Transformer;
}
mockTransformError.api = TRANSFORMER_API.VERSION;
module.exports = mockTransformError as Transformer;
import { Source } from "../../context";
import { Transformer } from "..";
/**
* Transformer returning a single mocked source.
*/
function mockTransform(): Iterable<Source> {
return [];
}
mockTransform.api = 0;
module.exports = mockTransform as Transformer;
import { Source } from "../../context";
import { Transformer } from "..";
import { Transformer, TRANSFORMER_API } from "..";
/**
* Transformer returning a single mocked source.
*/
module.exports = function mockTransform(source: Source) {
function mockTransform(source: Source): Iterable<Source> {
return [
{
data: `transformed source (was: ${source.data})`,
......@@ -14,4 +14,9 @@ module.exports = function mockTransform(source: Source) {
originalData: source.originalData || source.data,
},
];
} as Transformer;
}
/* mocks are always written against current version */
mockTransform.api = TRANSFORMER_API.VERSION;
module.exports = mockTransform as Transformer;
......@@ -8,3 +8,7 @@ export type Transformer = (
this: TransformContext,
source: Source
) => Iterable<Source>;
export enum TRANSFORMER_API {
VERSION = 1,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment