Merge pull request #8594 from goenning/go/add-custom-operators

[TechInsights] ability to add custom operators
This commit is contained in:
Patrik Oldsberg
2021-12-27 17:44:05 +01:00
committed by GitHub
5 changed files with 170 additions and 14 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-insights-backend-module-jsonfc': patch
---
ability to add custom operators
@@ -85,3 +85,34 @@ export const exampleCheck: TechInsightJsonRuleCheck = {
},
};
```
# Custom operators
json-rules-engine supports a limited [number of built-in operators](https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#operators) that can be used in conditions. You can add your own operators by adding them to the `operators` array in the `JsonRulesEngineFactCheckerFactory` constructor. For example:
```diff
+ import { Operator } from 'json-rules-engine';
const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({
checks: [],
logger,
+ operators: [ new Operator("startsWith", (a, b) => a.startsWith(b) ]
})
```
And you can then use it in your checks like this:
```js
...
rule: {
conditions: {
any: [
{
fact: 'version',
operator: 'startsWith',
value: '12',
},
],
},
}
```
@@ -8,6 +8,7 @@ import { CheckResponse } from '@backstage/plugin-tech-insights-common';
import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node';
import { FactChecker } from '@backstage/plugin-tech-insights-node';
import { Logger as Logger_2 } from 'winston';
import { Operator } from 'json-rules-engine';
import { TechInsightCheck } from '@backstage/plugin-tech-insights-node';
import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node';
import { TechInsightsStore } from '@backstage/plugin-tech-insights-node';
@@ -50,6 +51,7 @@ export class JsonRulesEngineFactChecker
repository,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerOptions);
// (undocumented)
getChecks(): Promise<TechInsightJsonRuleCheck[]>;
@@ -68,6 +70,7 @@ export class JsonRulesEngineFactCheckerFactory {
checks,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerFactoryOptions);
// (undocumented)
construct(repository: TechInsightsStore): JsonRulesEngineFactChecker;
@@ -78,6 +81,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = {
checks: TechInsightJsonRuleCheck[];
logger: Logger_2;
checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
operators?: Operator[];
};
// @public
@@ -86,6 +90,7 @@ export type JsonRulesEngineFactCheckerOptions = {
repository: TechInsightsStore;
logger: Logger_2;
checkRegistry?: TechInsightCheckRegistry<any>;
operators?: Operator[];
};
// @public (undocumented)
@@ -24,6 +24,7 @@ import {
} from '../index';
import { getVoidLogger } from '@backstage/backend-common';
import { TechInsightJsonRuleCheck } from '../types';
import { Operator } from 'json-rules-engine';
const testChecks: Record<string, TechInsightJsonRuleCheck[]> = {
broken: [
@@ -127,6 +128,49 @@ const testChecks: Record<string, TechInsightJsonRuleCheck[]> = {
},
},
],
customOperator: [
{
id: 'customOperatorTestCheck',
name: 'customOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
description: 'Check For Testing using Custom Operator',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
operator: 'isDivisibleBy',
value: 2,
},
],
},
},
},
],
invalidCustomOperator: [
{
id: 'invalidCustomOperatorTestCheck',
name: 'invalidCustomOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
description:
'Check For Testing using a Custom Operator that is not registered',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
operator: 'isOdd',
value: 2,
},
],
},
},
},
],
};
const latestSchemasMock = jest.fn().mockImplementation(() => [
@@ -166,6 +210,9 @@ describe('JsonRulesEngineFactChecker', () => {
const factChecker = new JsonRulesEngineFactCheckerFactory({
checkRegistry: mockCheckRegistry,
checks: [],
operators: [
new Operator<number, number>('isDivisibleBy', (a, b) => a % b === 0),
],
logger: getVoidLogger(),
}).construct(mockRepository);
@@ -234,6 +281,42 @@ describe('JsonRulesEngineFactChecker', () => {
});
});
it('should use custom operators when defined', async () => {
const results = await factChecker.runChecks('a/a/a', ['customOperator']);
expect(results).toHaveLength(1);
expect(results[0]).toMatchObject({
facts: {
testnumberfact: {
value: 3,
type: 'integer',
description: '',
},
},
result: false,
check: {
id: 'customOperatorTestCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'customOperatorTestCheck',
description: 'Check For Testing using Custom Operator',
factIds: ['test-factretriever'],
rule: {
conditions: {
all: [
{
fact: 'testnumberfact',
factResult: 3,
operator: 'isDivisibleBy',
result: false,
value: 2,
},
],
priority: 1,
},
},
},
});
});
it('should gracefully handle multiple check at once', async () => {
const results = await factChecker.runChecks('a/a/a', [
'simple',
@@ -307,17 +390,22 @@ describe('JsonRulesEngineFactChecker', () => {
});
describe('when validating checks', () => {
it('should succeed on valid rules', async () => {
const validationResponse = await factChecker.validate(
testChecks.simple[0],
);
expect(validationResponse.valid).toBeTruthy();
});
it('should fail on broken rules', async () => {
const validationResponse = await factChecker.validate(
testChecks.broken[0],
);
expect(validationResponse.valid).toBeFalsy();
[testChecks.simple[0], testChecks.customOperator[0]].forEach(check => {
it(`should succeed on valid rule: ${check.name}`, async () => {
const validationResponse = await factChecker.validate(check);
expect(validationResponse.valid).toBeTruthy();
});
});
[testChecks.broken[0], testChecks.invalidCustomOperator[0]].forEach(
check => {
it(`should fail on broken rules: ${check.name}`, async () => {
const validationResponse = await factChecker.validate(
testChecks.broken[0],
);
expect(validationResponse.valid).toBeFalsy();
});
},
);
});
});
@@ -23,11 +23,16 @@ import {
CheckValidationResponse,
} from '@backstage/plugin-tech-insights-node';
import { FactResponse } from '@backstage/plugin-tech-insights-common';
import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine';
import {
Engine,
EngineResult,
Operator,
TopLevelCondition,
} from 'json-rules-engine';
import { DefaultCheckRegistry } from './CheckRegistry';
import { Logger } from 'winston';
import { pick } from 'lodash';
import Ajv from 'ajv';
import Ajv, { SchemaObject } from 'ajv';
import * as validationSchema from './validation-schema.json';
import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants';
@@ -46,6 +51,7 @@ export type JsonRulesEngineFactCheckerOptions = {
repository: TechInsightsStore;
logger: Logger;
checkRegistry?: TechInsightCheckRegistry<any>;
operators?: Operator[];
};
/**
@@ -60,15 +66,27 @@ export class JsonRulesEngineFactChecker
private readonly checkRegistry: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
private repository: TechInsightsStore;
private readonly logger: Logger;
private readonly validationSchema: SchemaObject;
private readonly operators: Operator[];
constructor({
checks,
repository,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerOptions) {
this.repository = repository;
this.logger = logger;
this.operators = operators || [];
this.validationSchema = JSON.parse(JSON.stringify(validationSchema));
this.operators.forEach(op => {
this.validationSchema.definitions.condition.properties.operator.anyOf.push(
{ const: op.name },
);
});
checks.forEach(check => this.validate(check));
this.checkRegistry =
checkRegistry ??
@@ -80,6 +98,10 @@ export class JsonRulesEngineFactChecker
checks?: string[],
): Promise<JsonRuleBooleanCheckResult[]> {
const engine = new Engine();
this.operators.forEach(op => {
engine.addOperator(op);
});
const techInsightChecks = checks
? await this.checkRegistry.getAll(checks)
: await this.checkRegistry.list();
@@ -125,7 +147,7 @@ export class JsonRulesEngineFactChecker
check: TechInsightJsonRuleCheck,
): Promise<CheckValidationResponse> {
const ajv = new Ajv({ verbose: true });
const validator = ajv.compile(validationSchema);
const validator = ajv.compile(this.validationSchema);
const isValidToSchema = validator(check.rule);
if (check.type !== JSON_RULE_ENGINE_CHECK_TYPE) {
const msg = `Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker`;
@@ -317,6 +339,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = {
checks: TechInsightJsonRuleCheck[];
logger: Logger;
checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
operators?: Operator[];
};
/**
@@ -330,15 +353,18 @@ export class JsonRulesEngineFactCheckerFactory {
private readonly checks: TechInsightJsonRuleCheck[];
private readonly logger: Logger;
private readonly checkRegistry?: TechInsightCheckRegistry<TechInsightJsonRuleCheck>;
private readonly operators?: Operator[];
constructor({
checks,
logger,
checkRegistry,
operators,
}: JsonRulesEngineFactCheckerFactoryOptions) {
this.logger = logger;
this.checks = checks;
this.checkRegistry = checkRegistry;
this.operators = operators;
}
/**
@@ -352,6 +378,7 @@ export class JsonRulesEngineFactCheckerFactory {
logger: this.logger,
checkRegistry: this.checkRegistry,
repository,
operators: this.operators,
});
}
}