@sanity/json-match
Version:
A lightweight and lazy implementation of JSONMatch made for JavaScript
219 lines (184 loc) • 6.91 kB
text/typescript
import {describe, test, expect} from 'vitest'
import {parse} from './parse'
import {stringifyExpression} from './stringify'
describe('stringify', () => {
// Helper function to test round-trip: string -> AST -> string
function testRoundTrip(expression: string, expectedOutput?: string) {
const ast = parse(expression)
const stringified = stringifyExpression(ast)
const expected = expectedOutput || expression
expect(stringified).toBe(expected)
// Verify it can be parsed again (round-trip test)
const reparsed = parse(stringified)
expect(reparsed).toEqual(ast)
}
describe('Literal Expressions', () => {
test('stringifies boolean literals', () => {
expect(stringifyExpression(parse('true'))).toBe('true')
expect(stringifyExpression(parse('false'))).toBe('false')
})
test('stringifies null literal', () => {
expect(stringifyExpression(parse('null'))).toBe('null')
})
test('stringifies boolean literals in expressions', () => {
expect(stringifyExpression(parse('items[active == true]'))).toBe('items[active==true]')
expect(stringifyExpression(parse('items[visible != false]'))).toBe('items[visible!=false]')
})
test('stringifies null literal in expressions', () => {
expect(stringifyExpression(parse('items[value == null]'))).toBe('items[value==null]')
expect(stringifyExpression(parse('items[property != null]'))).toBe('items[property!=null]')
})
test('stringifies mixed boolean, null and other literals', () => {
expect(stringifyExpression(parse('[true, false, null, "text", 42]'))).toBe(
'[true,false,null,"text",42]',
)
})
})
describe('Basic Expressions', () => {
test('stringifies number literals', () => {
testRoundTrip('42')
testRoundTrip('-17')
testRoundTrip('3.14')
testRoundTrip('-99.9')
})
test('stringifies string literals', () => {
testRoundTrip('"hello"')
testRoundTrip('"escaped \\"quotes\\""')
testRoundTrip('"backslash \\\\ slash"')
testRoundTrip('"unicode \\u00E5 test"', '"unicode å test"')
})
test('stringifies simple identifiers', () => {
testRoundTrip('name')
testRoundTrip('user_name')
testRoundTrip('$variable')
testRoundTrip('_private')
})
test('stringifies context references', () => {
testRoundTrip('@')
testRoundTrip('$', '@')
})
test('stringifies wildcards', () => {
testRoundTrip('*')
})
})
describe('Path Expressions', () => {
test('stringifies dot notation', () => {
testRoundTrip('user.name')
testRoundTrip('data.items.metadata')
testRoundTrip('config.server.port')
})
test('stringifies recursive descent', () => {
testRoundTrip('data..name')
testRoundTrip('root..items.price')
testRoundTrip('..shortName', '@..shortName')
})
test('stringifies implicit root access', () => {
testRoundTrip('.bicycle', '@.bicycle')
testRoundTrip('.user.profile', '@.user.profile')
testRoundTrip('..price', '@..price')
testRoundTrip('..', '@..*')
})
})
describe('Array Access', () => {
test('stringifies array indices', () => {
testRoundTrip('items[0]')
testRoundTrip('users[-1]')
testRoundTrip('data[42]')
})
test('stringifies array slices', () => {
testRoundTrip('items[1:4]')
testRoundTrip('users[2:]', 'users[2:]')
testRoundTrip('data[:3]', 'data[:3]')
testRoundTrip('all[:]', 'all[*]')
})
test('stringifies wildcards in brackets', () => {
testRoundTrip('items[*]')
testRoundTrip('users[*].name')
})
test('stringifies multiple indices', () => {
testRoundTrip('items[0,2,4]')
testRoundTrip('data[1,3,5,7]')
})
test('stringifies mixed subscript content', () => {
testRoundTrip('items[0,2:4,*]')
testRoundTrip('data[1:3,5,7:9]')
})
})
describe('Constraints', () => {
test('stringifies comparison constraints', () => {
testRoundTrip('users[age>21]')
testRoundTrip('products[price<=100]')
testRoundTrip('items[status=="active"]')
testRoundTrip('numbers[@!=0]')
})
test('stringifies existence constraints', () => {
testRoundTrip('users[email?]')
testRoundTrip('items[metadata.tags?]')
testRoundTrip('products[category?]')
})
test('stringifies multiple constraints', () => {
testRoundTrip('users[age>65,age<18]')
testRoundTrip('items[type=="book",price<50]')
testRoundTrip('friends[age>35,favoriteColor=="blue"]')
})
test('stringifies context references in constraints', () => {
testRoundTrip('[@>10]')
testRoundTrip('[$<=5]', '[@<=5]')
testRoundTrip('[@==$]', '[@==@]')
})
})
describe('Quoted Identifiers', () => {
test('quotes identifiers that need quoting', () => {
const ast = parse("'field-name'")
expect(stringifyExpression(ast)).toBe("'field-name'")
})
test('does not quote simple identifiers', () => {
const ast = parse('simpleField')
expect(stringifyExpression(ast)).toBe('simpleField')
})
test('handles escaped quotes in identifiers', () => {
testRoundTrip("'escaped \\'quotes\\''")
testRoundTrip('\'mixed "quotes"\'')
})
test('handles special characters in identifiers', () => {
testRoundTrip("'special-chars'")
testRoundTrip("'unicode-\\u00E5'", "'unicode-å'")
testRoundTrip("'control\\nchars'", "'control\\nchars'")
})
})
describe('Complex Expressions', () => {
test('stringifies complex nested expressions', () => {
testRoundTrip('data.items[*].tags[0]')
testRoundTrip('users[age>21].profile.email')
testRoundTrip('products[category=="electronics"][price>100].name')
})
test('stringifies expressions with mixed access patterns', () => {
testRoundTrip('matrix[1][2]')
testRoundTrip('data.items[*][0]')
testRoundTrip('users[*].addresses[0].city')
})
test('stringifies real-world examples', () => {
testRoundTrip('friends[age>35,favoriteColor=="blue"]')
testRoundTrip('products[1:3,price>500]')
testRoundTrip('[zargh,blagh,fnargh[1,2,3]]')
testRoundTrip('.bicycle.*', '@.bicycle.*')
})
})
describe('Edge Cases', () => {
test('handles numeric edge cases', () => {
testRoundTrip('0')
testRoundTrip('3.0', '3')
})
test('handles string escaping edge cases', () => {
testRoundTrip('"\\b\\f\\n\\r\\t"')
testRoundTrip('"\\u0000\\u001F"', '"\\u0000\\u001f"')
testRoundTrip('"\\\\"')
})
})
describe('Error Cases', () => {
test('throws on unknown node types', () => {
const invalidNode = {type: 'InvalidType'} as any
expect(() => stringifyExpression(invalidNode)).toThrow('Unknown node type: InvalidType')
})
})
})