shape-filter.test.ts
1 import { describe, expect, it } from 'vitest'; 2 import type { Shape } from './shape.js'; 3 import { 4 collectShapeSegments, 5 extractSegments, 6 parseFilter, 7 shapeMatchesFilter, 8 } from './shape-filter.js'; 9 10 describe('parseFilter', () => { 11 it('splits comma-separated fields, trims, and drops empty tokens', () => { 12 const r = parseFilter('author, text , likes'); 13 expect(r).toEqual({ fields: ['author', 'text', 'likes'] }); 14 }); 15 16 it('dedupes while preserving first-seen order', () => { 17 const r = parseFilter('a,b,a,c,b'); 18 expect(r).toEqual({ fields: ['a', 'b', 'c'] }); 19 }); 20 21 it('rejects empty string as invalid_filter', () => { 22 const r = parseFilter(''); 23 expect(r).toMatchObject({ reason: expect.stringContaining('non-empty') }); 24 }); 25 26 it('rejects whitespace-only as invalid_filter', () => { 27 const r = parseFilter(' '); 28 expect(r).toMatchObject({ reason: expect.stringContaining('non-empty') }); 29 }); 30 31 it('rejects commas-only as invalid_filter', () => { 32 const r = parseFilter(',,,'); 33 expect(r).toMatchObject({ reason: expect.stringContaining('non-empty') }); 34 }); 35 36 it('accepts a single field', () => { 37 expect(parseFilter('author')).toEqual({ fields: ['author'] }); 38 }); 39 }); 40 41 describe('extractSegments', () => { 42 it('returns empty for root', () => { 43 expect(extractSegments('$')).toEqual([]); 44 }); 45 46 it('splits dotted path and drops $', () => { 47 expect(extractSegments('$.data.user.name')).toEqual(['data', 'user', 'name']); 48 }); 49 50 it('drops numeric array indices', () => { 51 expect(extractSegments('$.items[0].author')).toEqual(['items', 'author']); 52 expect(extractSegments('$.rows[0][12]')).toEqual(['rows']); 53 }); 54 55 it('unwraps bracket-quoted keys', () => { 56 expect(extractSegments('$.data["weird key"]')).toEqual(['data', 'weird key']); 57 }); 58 59 it('handles bracket-quoted keys at root', () => { 60 expect(extractSegments('$["123bad"]')).toEqual(['123bad']); 61 }); 62 63 it('mixes bracket keys and dot segments', () => { 64 expect(extractSegments('$.data.user["nick name"].age')) 65 .toEqual(['data', 'user', 'nick name', 'age']); 66 }); 67 }); 68 69 describe('collectShapeSegments', () => { 70 it('collects every segment name from every path in a shape', () => { 71 const shape: Shape = { 72 '$': 'object', 73 '$.data': 'object', 74 '$.data.items': 'array(3)', 75 '$.data.items[0]': 'object', 76 '$.data.items[0].author': 'string', 77 '$.data.items[0].text': 'string', 78 }; 79 const segs = collectShapeSegments(shape); 80 expect(segs.has('data')).toBe(true); 81 expect(segs.has('items')).toBe(true); 82 expect(segs.has('author')).toBe(true); 83 expect(segs.has('text')).toBe(true); 84 expect(segs.has('$')).toBe(false); 85 expect(segs.has('[0]')).toBe(false); 86 }); 87 88 it('returns an empty set for an empty shape', () => { 89 expect(collectShapeSegments({}).size).toBe(0); 90 }); 91 }); 92 93 describe('shapeMatchesFilter', () => { 94 const shape: Shape = { 95 '$': 'object', 96 '$.data': 'object', 97 '$.data.items': 'array(1)', 98 '$.data.items[0].author': 'string', 99 '$.data.items[0].text': 'string', 100 '$.data.items[0].likes': 'number', 101 }; 102 103 it('returns true when every field matches some path segment (AND)', () => { 104 expect(shapeMatchesFilter(shape, ['author', 'text', 'likes'])).toBe(true); 105 }); 106 107 it('matches nested container names, not just leaves (any-segment rule)', () => { 108 // `data` and `items` are container segments, not leaves; still must match. 109 expect(shapeMatchesFilter(shape, ['data', 'items'])).toBe(true); 110 }); 111 112 it('returns false when any field is missing', () => { 113 expect(shapeMatchesFilter(shape, ['author', 'missing'])).toBe(false); 114 }); 115 116 it('is case-sensitive', () => { 117 expect(shapeMatchesFilter(shape, ['Author'])).toBe(false); 118 expect(shapeMatchesFilter(shape, ['author'])).toBe(true); 119 }); 120 121 it('empty filter list vacuously matches', () => { 122 expect(shapeMatchesFilter(shape, [])).toBe(true); 123 }); 124 125 it('rejects requests whose shape has no body (empty shape)', () => { 126 expect(shapeMatchesFilter({}, ['author'])).toBe(false); 127 }); 128 });