Merge pull request #8594 from goenning/go/add-custom-operators
[TechInsights] ability to add custom operators
This commit is contained in:
@@ -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)
|
||||
|
||||
+99
-11
@@ -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();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+30
-3
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user