Files
ersatztv/web/src/builder/rules/parse.test.ts
T

75 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { parse } from './parse';
import { compile } from './compile';
import type { FieldType } from './types';
const FIELDS: Record<string, FieldType> = {
genre: 'text', title: 'text', plot: 'fulltext', type: 'enum', minutes: 'number', release_date: 'date'
};
describe('parse', () => {
it('parses a single quoted text atom as is', () => {
expect(parse('genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'is', value: 'Horror' }] });
});
it('parses NOT as isNot / notMatches by field type', () => {
expect(parse('NOT genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'isNot', value: 'Horror' }] });
expect(parse('NOT plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'notMatches', value: 'car' }] });
});
it('parses a quoted fulltext atom as matches', () => {
expect(parse('plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'matches', value: 'car' }] });
});
it('parses wildcard forms', () => {
expect(parse('title:*night*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'contains', value: 'night' }] });
expect(parse('title:The*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'startsWith', value: 'The' }] });
});
it('parses number/date ranges disambiguated by field type', () => {
expect(parse('minutes:{30 TO *}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'gt', value: '30' }] });
expect(parse('release_date:{* TO 2010-01-01}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'release_date', operator: 'before', value: '2010-01-01' }] });
expect(parse('minutes:[30 TO 90]', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'between', value: '30', value2: '90' }] });
expect(parse('minutes:42', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'eq', value: '42' }] });
});
it('parses a nested group', () => {
expect(parse('type:"movie" AND (genre:"Horror" OR genre:"Thriller")', FIELDS)).toEqual({
match: 'all',
children: [
{ field: 'type', operator: 'is', value: 'movie' },
{ match: 'any', children: [
{ field: 'genre', operator: 'is', value: 'Horror' },
{ field: 'genre', operator: 'is', value: 'Thriller' }
] }
]
});
});
it('returns null on out-of-subset input', () => {
expect(parse('genre:"a" AND genre:"b" OR genre:"c"', FIELDS)).toBeNull(); // mixed AND/OR at one level
expect(parse('unknown_field:"x"', FIELDS)).toBeNull(); // unknown field
expect(parse('genre:"a" AND (type:"movie" AND (genre:"b"))', FIELDS)).toBeNull(); // two levels of nesting
expect(parse('title:jo~2', FIELDS)).toBeNull(); // fuzzy — not in subset
});
});
describe('parse: special-char round-trips', () => {
it('round-trips quoted values with lucene specials (compile∘parse)', () => {
for (const v of ['a*b', 'x y', 'a\\', 'has"quote', '(paren)', 'a AND b', 'a:b']) {
const g = { match: 'all' as const, children: [{ field: 'genre', operator: 'is' as const, value: v }] };
expect(parse(compile(g), FIELDS)).toEqual(g);
}
});
it('round-trips text contains/startsWith values with specials', () => {
for (const v of ['a*b', 'x y', 'plain']) {
const c = { match: 'all' as const, children: [{ field: 'title', operator: 'contains' as const, value: v }] };
expect(parse(compile(c), FIELDS)).toEqual(c);
}
for (const v of ['star*', 'x y', 'ab']) {
const s = { match: 'all' as const, children: [{ field: 'title', operator: 'startsWith' as const, value: v }] };
expect(parse(compile(s), FIELDS)).toEqual(s);
}
});
});