Skip to content

Commit e6c54e9

Browse files
committed
feat: add filter ts impl and test suite
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent ab7fb52 commit e6c54e9

File tree

8 files changed

+1214
-644
lines changed

8 files changed

+1214
-644
lines changed

filter/filter.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { validateStringFilter, checkStringFilterMatch } from './filter.js'
3+
import { StringFilter } from './filter.pb.js'
4+
5+
describe('validateStringFilter', () => {
6+
it('should return null for null/undefined filter', () => {
7+
expect(validateStringFilter(null)).toBeNull()
8+
expect(validateStringFilter(undefined)).toBeNull()
9+
})
10+
11+
it('should return null for empty filter', () => {
12+
expect(validateStringFilter({})).toBeNull()
13+
})
14+
15+
it('should return null for valid regex', () => {
16+
expect(validateStringFilter({ re: '^test.*' })).toBeNull()
17+
expect(validateStringFilter({ re: '[0-9]+' })).toBeNull()
18+
})
19+
20+
it('should return error message for invalid regex', () => {
21+
const result = validateStringFilter({ re: '[' })
22+
expect(result).toContain('re:')
23+
expect(result).toContain('Invalid regular expression')
24+
})
25+
26+
it('should handle regex with escape sequences', () => {
27+
expect(validateStringFilter({ re: '\\d+' })).toBeNull()
28+
})
29+
})
30+
31+
describe('checkStringFilterMatch', () => {
32+
it('should return true for null/undefined filter', () => {
33+
expect(checkStringFilterMatch(null, 'test')).toBe(true)
34+
expect(checkStringFilterMatch(undefined, 'test')).toBe(true)
35+
})
36+
37+
it('should return true for empty filter', () => {
38+
expect(checkStringFilterMatch({}, 'test')).toBe(true)
39+
})
40+
41+
describe('empty filter', () => {
42+
it('should match empty string when empty=true', () => {
43+
expect(checkStringFilterMatch({ empty: true }, '')).toBe(true)
44+
})
45+
46+
it('should not match non-empty string when empty=true', () => {
47+
expect(checkStringFilterMatch({ empty: true }, 'test')).toBe(false)
48+
})
49+
})
50+
51+
describe('notEmpty filter', () => {
52+
it('should match non-empty string when notEmpty=true', () => {
53+
expect(checkStringFilterMatch({ notEmpty: true }, 'test')).toBe(true)
54+
})
55+
56+
it('should not match empty string when notEmpty=true', () => {
57+
expect(checkStringFilterMatch({ notEmpty: true }, '')).toBe(false)
58+
})
59+
})
60+
61+
describe('value filter', () => {
62+
it('should match exact value', () => {
63+
expect(checkStringFilterMatch({ value: 'test' }, 'test')).toBe(true)
64+
})
65+
66+
it('should not match different value', () => {
67+
expect(checkStringFilterMatch({ value: 'test' }, 'other')).toBe(false)
68+
})
69+
})
70+
71+
describe('values filter', () => {
72+
it('should match when value is in values array', () => {
73+
expect(checkStringFilterMatch({ values: ['test1', 'test2'] }, 'test1')).toBe(true)
74+
expect(checkStringFilterMatch({ values: ['test1', 'test2'] }, 'test2')).toBe(true)
75+
})
76+
77+
it('should not match when value is not in values array', () => {
78+
expect(checkStringFilterMatch({ values: ['test1', 'test2'] }, 'test3')).toBe(false)
79+
})
80+
81+
it('should return true when values array is empty', () => {
82+
expect(checkStringFilterMatch({ values: [] }, 'test')).toBe(true)
83+
})
84+
})
85+
86+
describe('regex filter', () => {
87+
it('should match when regex matches', () => {
88+
expect(checkStringFilterMatch({ re: '^test' }, 'test123')).toBe(true)
89+
expect(checkStringFilterMatch({ re: '\\d+' }, 'abc123')).toBe(true)
90+
})
91+
92+
it('should not match when regex does not match', () => {
93+
expect(checkStringFilterMatch({ re: '^test' }, 'abc123')).toBe(false)
94+
expect(checkStringFilterMatch({ re: '\\d+' }, 'abcdef')).toBe(false)
95+
})
96+
97+
it('should return false for invalid regex', () => {
98+
expect(checkStringFilterMatch({ re: '[' }, 'test')).toBe(false)
99+
})
100+
})
101+
102+
describe('hasPrefix filter', () => {
103+
it('should match when string has prefix', () => {
104+
expect(checkStringFilterMatch({ hasPrefix: 'test' }, 'test123')).toBe(true)
105+
})
106+
107+
it('should not match when string does not have prefix', () => {
108+
expect(checkStringFilterMatch({ hasPrefix: 'test' }, 'abc123')).toBe(false)
109+
})
110+
})
111+
112+
describe('hasSuffix filter', () => {
113+
it('should match when string has suffix', () => {
114+
expect(checkStringFilterMatch({ hasSuffix: '123' }, 'test123')).toBe(true)
115+
})
116+
117+
it('should not match when string does not have suffix', () => {
118+
expect(checkStringFilterMatch({ hasSuffix: '123' }, 'test456')).toBe(false)
119+
})
120+
})
121+
122+
describe('contains filter', () => {
123+
it('should match when string contains substring', () => {
124+
expect(checkStringFilterMatch({ contains: 'est' }, 'test123')).toBe(true)
125+
})
126+
127+
it('should not match when string does not contain substring', () => {
128+
expect(checkStringFilterMatch({ contains: 'xyz' }, 'test123')).toBe(false)
129+
})
130+
})
131+
132+
describe('combined filters', () => {
133+
it('should match when all filters match', () => {
134+
expect(checkStringFilterMatch({
135+
notEmpty: true,
136+
hasPrefix: 'test',
137+
hasSuffix: '123'
138+
}, 'test123')).toBe(true)
139+
})
140+
141+
it('should not match when any filter fails', () => {
142+
expect(checkStringFilterMatch({
143+
notEmpty: true,
144+
hasPrefix: 'test',
145+
hasSuffix: '456'
146+
}, 'test123')).toBe(false)
147+
})
148+
149+
it('should handle complex combination', () => {
150+
expect(checkStringFilterMatch({
151+
re: '^test',
152+
contains: '12',
153+
hasSuffix: '3'
154+
}, 'test123')).toBe(true)
155+
})
156+
})
157+
})

filter/filter.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { StringFilter } from './filter.pb.js'
2+
3+
/**
4+
* Validates the string filter.
5+
* @param filter The string filter to validate
6+
* @returns Error message if invalid, null if valid
7+
*/
8+
export function validateStringFilter(
9+
filter: StringFilter | null | undefined,
10+
): string | null {
11+
if (!filter) {
12+
return null
13+
}
14+
15+
if (filter.re) {
16+
try {
17+
new RegExp(filter.re)
18+
} catch (error) {
19+
return `re: ${error instanceof Error ? error.message : 'invalid regex'}`
20+
}
21+
}
22+
23+
return null
24+
}
25+
26+
/**
27+
* Checks if the given value matches the string filter.
28+
* All of the non-zero rules must match for the filter to match.
29+
* An empty filter matches any.
30+
* @param filter The string filter to check against
31+
* @param value The string value to check
32+
* @returns True if the value matches the filter, false otherwise
33+
*/
34+
export function checkStringFilterMatch(
35+
filter: StringFilter | null | undefined,
36+
value: string,
37+
): boolean {
38+
if (!filter) {
39+
return true
40+
}
41+
42+
if (filter.empty && value !== '') {
43+
return false
44+
}
45+
46+
if (filter.notEmpty && value === '') {
47+
return false
48+
}
49+
50+
if (filter.value && value !== filter.value) {
51+
return false
52+
}
53+
54+
if (
55+
filter.values &&
56+
filter.values.length > 0 &&
57+
!filter.values.includes(value)
58+
) {
59+
return false
60+
}
61+
62+
if (filter.re) {
63+
try {
64+
const regex = new RegExp(filter.re)
65+
if (!regex.test(value)) {
66+
return false
67+
}
68+
} catch {
69+
// Invalid regex treated as a fail (checked in validate but treat as fail)
70+
return false
71+
}
72+
}
73+
74+
if (filter.hasPrefix && !value.startsWith(filter.hasPrefix)) {
75+
return false
76+
}
77+
78+
if (filter.hasSuffix && !value.endsWith(filter.hasSuffix)) {
79+
return false
80+
}
81+
82+
if (filter.contains && !value.includes(filter.contains)) {
83+
return false
84+
}
85+
86+
return true
87+
}

0 commit comments

Comments
 (0)