Negation key word to entity filter

Signed-off-by: Jordan Snow <jordans@spotify.com>
This commit is contained in:
Jordan Snow
2024-08-29 17:16:36 +02:00
parent 2d50c4793d
commit 875ceaee9a
6 changed files with 52 additions and 10 deletions
@@ -16,6 +16,7 @@
import { InputError } from '@backstage/errors';
import { EntityMatcherFn } from './types';
import { Entity } from '@backstage/catalog-model';
const allowedMatchers: Record<string, EntityMatcherFn> = {
labels: entity => {
@@ -32,6 +33,7 @@ const allowedMatchers: Record<string, EntityMatcherFn> = {
export function createHasMatcher(
parameters: string[],
onParseError: (error: Error) => void,
negation?: boolean,
): EntityMatcherFn {
const matchers = parameters.flatMap(parameter => {
const matcher = allowedMatchers[parameter.toLocaleLowerCase('en-US')];
@@ -47,6 +49,7 @@ export function createHasMatcher(
return [matcher];
});
return entity =>
const isMatch = (entity: Entity) =>
matchers.length ? matchers.some(matcher => matcher(entity)) : true;
return negation ? entity => !isMatch(entity) : isMatch;
}
@@ -28,6 +28,7 @@ const allowedMatchers: Record<string, EntityMatcherFn> = {
export function createIsMatcher(
parameters: string[],
onParseError: (error: Error) => void,
negation?: boolean,
): EntityMatcherFn {
const matchers = parameters.flatMap(parameter => {
const matcher = allowedMatchers[parameter.toLocaleLowerCase('en-US')];
@@ -43,6 +44,7 @@ export function createIsMatcher(
return [matcher];
});
return entity =>
const isMatch = (entity: any) =>
matchers.length ? matchers.some(matcher => matcher(entity)) : true;
return negation ? entity => !isMatch(entity) : isMatch;
}
@@ -22,7 +22,10 @@ import { EntityMatcherFn } from './types';
export function createKindMatcher(
parameters: string[],
_onParseError: (error: Error) => void,
negation?: boolean,
): EntityMatcherFn {
const items = parameters.map(p => p.toLocaleLowerCase('en-US'));
return entity => items.includes(entity.kind.toLocaleLowerCase('en-US'));
const isMatch = (entity: any) =>
items.includes(entity.kind.toLocaleLowerCase('en-US'));
return negation ? entity => !isMatch(entity) : isMatch;
}
@@ -22,13 +22,14 @@ import { EntityMatcherFn } from './types';
export function createTypeMatcher(
parameters: string[],
_onParseError: (error: Error) => void,
negation?: boolean,
): EntityMatcherFn {
const items = parameters.map(p => p.toLocaleLowerCase('en-US'));
return entity => {
const value = entity.spec?.type;
return (
const isMatch =
typeof value === 'string' &&
items.includes(value.toLocaleLowerCase('en-US'))
);
items.includes(value.toLocaleLowerCase('en-US'));
return negation ? !isMatch : isMatch;
};
}
@@ -93,6 +93,20 @@ describe('parseFilterExpression', () => {
);
});
it('recognizes negation key', () => {
const component = { kind: 'Component' } as unknown as Entity;
expect(run('not:kind:user')(component)).toBe(true);
});
it('supports negation and affirmative expressions', () => {
const component = {
kind: 'Component',
spec: { type: 'service' },
} as unknown as Entity;
expect(run('not:kind:user type:service')(component)).toBe(true);
expect(run('type:service not:kind:user')(component)).toBe(true);
});
it('rejects unknown keys', () => {
expect(() => run('unknown:foo')).toThrowErrorMatchingInlineSnapshot(
`"'unknown' is not a valid filter expression key, expected one of 'kind','type','is','has'"`,
@@ -27,6 +27,7 @@ const rootMatcherFactories: Record<
(
parameters: string[],
onParseError: (error: Error) => void,
negation?: boolean,
) => EntityMatcherFn
> = {
kind: createKindMatcher,
@@ -60,10 +61,14 @@ export function parseFilterExpression(expression: string): {
const parts = splitFilterExpression(expression, e =>
expressionParseErrors.push(e),
);
let negation = false;
const matchers = parts.flatMap(part => {
const factory = rootMatcherFactories[part.key];
if (!factory) {
if (isNegation(part.key)) {
negation = true;
return [];
}
const known = Object.keys(rootMatcherFactories).map(m => `'${m}'`);
expressionParseErrors.push(
new InputError(
@@ -73,9 +78,12 @@ export function parseFilterExpression(expression: string): {
return [];
}
const matcher = factory(part.parameters, e =>
expressionParseErrors.push(e),
const matcher = factory(
part.parameters,
e => expressionParseErrors.push(e),
negation,
);
negation = false;
return [matcher];
});
@@ -106,7 +114,11 @@ export function splitFilterExpression(
const result = new Array<{ key: string; parameters: string[] }>();
for (const word of words) {
const match = word.match(/^([^:]+):(.+)$/);
let match = word.match(/^([^:]+):(.+)$/);
if (match && isNegation(match[1])) {
result.push({ key: 'not', parameters: [] });
match = match[2].match(/^([^:]+):(.+)$/) || null;
}
if (!match) {
onParseError(
new InputError(
@@ -124,3 +136,10 @@ export function splitFilterExpression(
return result;
}
function isNegation(parameter: string): boolean {
if (parameter.toLocaleLowerCase('en-US') === 'not') {
return true;
}
return false;
}