M1: core path escaping, order-preserving index value encoding, errors
Scaffold TypeScript/Vitest project, add DESIGN.md (brief) and CLAUDE.md. Property tests cover escape round-trips, separator-freedom and order preservation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import fc from 'fast-check'
|
||||
import { decodeIndexValue, encodeIndexValue, type IndexValue } from '../../src/core/encoding.js'
|
||||
|
||||
const separators = fc.constantFrom('/', ':', '.', '|', '#', '!')
|
||||
|
||||
const indexValue: fc.Arbitrary<IndexValue> = fc.oneof(
|
||||
fc.constant(null),
|
||||
fc.boolean(),
|
||||
fc.double({ noNaN: true }),
|
||||
fc.integer(),
|
||||
fc.string({ unit: 'binary' }),
|
||||
fc.string({ unit: fc.constantFrom('/', ':', '0', '1', 'a', '%', '~', '\0', '!') }),
|
||||
)
|
||||
|
||||
const rank = (v: IndexValue) => (v === null ? 0 : v === false ? 1 : v === true ? 2 : typeof v === 'number' ? 3 : 4)
|
||||
|
||||
function compare(a: IndexValue, b: IndexValue): number {
|
||||
const ra = rank(a)
|
||||
const rb = rank(b)
|
||||
if (ra !== rb) return ra - rb
|
||||
if (typeof a === 'number' && typeof b === 'number') return a === b ? 0 : a < b ? -1 : 1
|
||||
if (typeof a === 'string' && typeof b === 'string') return a === b ? 0 : a < b ? -1 : 1
|
||||
return 0
|
||||
}
|
||||
|
||||
const sign = (n: number) => (n < 0 ? -1 : n > 0 ? 1 : 0)
|
||||
const cmpStr = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
describe('encodeIndexValue', () => {
|
||||
it('round-trips (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(indexValue, separators, (v, sep) => {
|
||||
const decoded = decodeIndexValue(encodeIndexValue(v, sep), sep)
|
||||
if (typeof v === 'number' && v === 0) expect(decoded).toBe(0) // -0 normalised
|
||||
else expect(decoded).toEqual(v)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits only characters above the separator (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(indexValue, separators, (v, sep) => {
|
||||
for (const c of encodeIndexValue(v, sep)) expect(c > sep).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves order, including when followed by a separator and suffix (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(indexValue, indexValue, fc.string(), fc.string(), separators, (a, b, sa, sb, sep) => {
|
||||
const ea = encodeIndexValue(a, sep)
|
||||
const eb = encodeIndexValue(b, sep)
|
||||
const expected = sign(compare(a, b))
|
||||
expect(cmpStr(ea, eb)).toBe(expected)
|
||||
if (expected !== 0) expect(cmpStr(ea + sep + sa, eb + sep + sb)).toBe(expected)
|
||||
}),
|
||||
{ numRuns: 2000 },
|
||||
)
|
||||
})
|
||||
|
||||
it('orders types null < false < true < numbers < strings', () => {
|
||||
const values: IndexValue[] = ['', 'a', -Infinity, -1, 0, 1e300, Infinity, true, false, null]
|
||||
const sorted = [...values].sort((a, b) => cmpStr(encodeIndexValue(a), encodeIndexValue(b)))
|
||||
expect(sorted).toEqual([null, false, true, -Infinity, -1, 0, 1e300, Infinity, '', 'a'])
|
||||
})
|
||||
|
||||
it('rejects NaN and non-index values', () => {
|
||||
expect(() => encodeIndexValue(NaN)).toThrow()
|
||||
expect(() => encodeIndexValue({} as never)).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import fc from 'fast-check'
|
||||
import {
|
||||
escapeSegment,
|
||||
joinPath,
|
||||
pathKeys,
|
||||
splitPath,
|
||||
succ,
|
||||
toSegment,
|
||||
unescapeSegment,
|
||||
validateSeparator,
|
||||
} from '../../src/core/path.js'
|
||||
import { InvalidKeyError } from '../../src/errors.js'
|
||||
|
||||
const separators = fc.constantFrom('/', ':', '.', '|', '#', '\\', '!')
|
||||
|
||||
describe('escapeSegment', () => {
|
||||
it('escapes %, the separator and a leading ~', () => {
|
||||
expect(escapeSegment('a/b')).toBe('a%2Fb')
|
||||
expect(escapeSegment('50%')).toBe('50%25')
|
||||
expect(escapeSegment('~x')).toBe('%7Ex')
|
||||
expect(escapeSegment('x~')).toBe('x~')
|
||||
expect(escapeSegment('a:b', ':')).toBe('a%3Ab')
|
||||
expect(escapeSegment('a/b', ':')).toBe('a/b')
|
||||
expect(escapeSegment('plain')).toBe('plain')
|
||||
})
|
||||
|
||||
it('round-trips (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.string({ unit: 'binary' }), separators, (s, sep) => {
|
||||
expect(unescapeSegment(escapeSegment(s, sep))).toBe(s)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('never emits a raw separator or a leading ~ (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.string({ unit: 'binary' }), separators, (s, sep) => {
|
||||
const e = escapeSegment(s, sep)
|
||||
expect(e.includes(sep)).toBe(false)
|
||||
expect(e.startsWith('~')).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('joins and splits paths (property)', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.array(fc.string({ minLength: 1 }), { maxLength: 6 }), separators, (path, sep) => {
|
||||
expect(splitPath(joinPath(path, sep), sep)).toEqual(path)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pathKeys', () => {
|
||||
it('precomputes selfKey, prefix and subtree end', () => {
|
||||
expect(pathKeys(['users', '123'], '/')).toMatchObject({ selfKey: 'users/123', prefix: 'users/123/', end: 'users/1230' })
|
||||
expect(pathKeys([], '/')).toMatchObject({ selfKey: '', prefix: '', end: undefined })
|
||||
expect(pathKeys(['a/b'], '/').selfKey).toBe('a%2Fb')
|
||||
})
|
||||
})
|
||||
|
||||
describe('misc', () => {
|
||||
it('succ returns the next char', () => {
|
||||
expect(succ('/')).toBe('0')
|
||||
})
|
||||
|
||||
it('toSegment rejects undefined, null, empty and non-finite', () => {
|
||||
expect(() => toSegment(undefined)).toThrow(InvalidKeyError)
|
||||
expect(() => toSegment(null)).toThrow(TypeError)
|
||||
expect(() => toSegment('')).toThrow(InvalidKeyError)
|
||||
expect(() => toSegment(NaN)).toThrow(InvalidKeyError)
|
||||
expect(toSegment(12)).toBe('12')
|
||||
})
|
||||
|
||||
it('validates separators', () => {
|
||||
expect(() => validateSeparator('//')).toThrow()
|
||||
expect(() => validateSeparator('%')).toThrow()
|
||||
expect(() => validateSeparator('~')).toThrow()
|
||||
expect(() => validateSeparator(':')).not.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user