Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/pure-parse/src/memoization/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
array,
// nonEmptyArray,
object,
objectNoJit,
// partialRecord,
// record,
// tuple,
Expand All @@ -12,6 +13,7 @@ import {

export const unionMemo = memoizeValidatorConstructor(oneOf)
export const objectMemo = memoizeValidatorConstructor(object)
export const objectNoJitMemo = memoizeValidatorConstructor(objectNoJit)
// TODO export const recordMemo = memoizeValidatorConstructor(record)
// TODO export const partialRecordMemo = memoizeValidatorConstructor(partialRecord)
export const tupleMemo = memoizeValidatorConstructor(tuple)
Expand Down
75 changes: 74 additions & 1 deletion packages/pure-parse/src/parsers/arrays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { failure, success } from './types'
import { literal } from './literal'
import { oneOf } from './oneOf'
import { always } from './always'
import { parseString } from './primitives'

describe('arrays', () => {
it('validates when all elements pass validation', () => {
Expand Down Expand Up @@ -41,5 +42,77 @@ describe('arrays', () => {
}),
)
})
describe.todo('self-referential arrays')
describe('errors', () => {
it('reports non-arrays', () => {
const parse = array(parseString)
expect(parse(1)).toEqual(
expect.objectContaining({
tag: 'failure',
path: [],
}),
)
})
describe('nested errors', () => {
it('reports shallow errors in elements', () => {
const parse = array(parseString)
expect(parse([1])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [{ tag: 'array', index: 0 }],
}),
)
})
it('reports deep errors in nested elements', () => {
const parse = array(array(parseString))
expect(parse([[1]])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{ tag: 'array', index: 0 },
{ tag: 'array', index: 0 },
],
}),
)
expect(parse([[], [], [1]])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{ tag: 'array', index: 2 },
{ tag: 'array', index: 0 },
],
}),
)
expect(parse([[], ['a'], ['a', 'b', 'c', 3], []])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{ tag: 'array', index: 2 },
{ tag: 'array', index: 3 },
],
}),
)
})
test('that the index is accurate', () => {
const parse = array(parseString)
expect(parse([1, 2, 3])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [{ tag: 'array', index: 0 }],
}),
)
expect(parse(['1', 2, 3])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [{ tag: 'array', index: 1 }],
}),
)
expect(parse(['1', '2', 3])).toEqual(
expect.objectContaining({
tag: 'failure',
path: [{ tag: 'array', index: 2 }],
}),
)
})
})
})
})
32 changes: 10 additions & 22 deletions packages/pure-parse/src/parsers/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,28 @@ import {
Parser,
ParseResult,
success,
propagateFailure,
} from './types'

// Local helper function
const areAllSuccesses = <T>(
results: ParseResult<T>[],
): results is ParseSuccess<T>[] => results.every((result) => isSuccess(result))

/**
* Validate arrays
* @return a function that parses arrays
* @param parseItem
*/
export const array =
<T>(parseItem: Parser<T>): Parser<T[]> =>
(data: unknown) => {
(data) => {
if (!Array.isArray(data)) {
return failure('Not an array')
}
const results: ParseResult<T>[] = data.map(parseItem)

// Imperative programming for performance
let allSuccess = true
for (const result of results) {
allSuccess &&= result.tag !== 'failure'
}

if (!allSuccess) {
return failure('Not all elements are valid')
const dataOutput = []
for (let i = 0; i < data.length; i++) {
const parseResult = parseItem(data[i])
if (parseResult.tag === 'failure') {
return propagateFailure(parseResult, { tag: 'array', index: i })
}
dataOutput.push((parseResult as ParseSuccess<unknown>).value)
}

// If any element is a fallbackValue, return a new array
return success(
(results as Array<Exclude<ParseResult<T>, { tag: 'failure' }>>).map(
(result) => result.value,
),
)
return success(dataOutput as T[])
}
87 changes: 82 additions & 5 deletions packages/pure-parse/src/parsers/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,28 @@ import { Infer } from '../common'
import { literal } from './literal'
import { nullable, optional } from './optional'
import { always } from './always'
import { objectMemo, objectNoJitMemo } from '../memoization'

const suits = [
const suites = [
{
name: 'objectNoEval',
name: 'object without JIT',
fn: objectNoJit,
},
{
name: 'objectEval',
name: 'object with JIT',
fn: objectParseEval,
},
// TODO objectMemo
{
name: 'memoized object with JIT',
fn: objectMemo,
},
{
name: 'memoized object without JIT',
fn: objectNoJitMemo,
},
]

suits.forEach(({ name: suiteName, fn: object }) => {
suites.forEach(({ name: suiteName, fn: object }) => {
describe(suiteName, () => {
describe('objects', () => {
describe('unknown properties', () => {
Expand Down Expand Up @@ -371,6 +379,75 @@ suits.forEach(({ name: suiteName, fn: object }) => {
})(data)
})
})
describe('errors', () => {
it('reports non-objects', () => {
const parse = object({
a: parseString,
})
expect(parse('nonanobj')).toEqual(
expect.objectContaining({
tag: 'failure',
path: [],
}),
)
})
it('reports missing properties', () => {
const parse = object({
a: parseString,
})
expect(parse({})).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{
tag: 'object',
key: 'a',
},
],
}),
)
})
describe('nested errors', () => {
it('reports shallow errors in properties', () => {
const parse = object({
a: parseString,
})
expect(parse({ a: 1 })).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{
tag: 'object',
key: 'a',
},
],
}),
)
})
it('reports deep errors in nested properties', () => {
const parse = object({
a: object({
b: parseString,
}),
})
expect(parse({ a: { b: 1 } })).toEqual(
expect.objectContaining({
tag: 'failure',
path: [
{
tag: 'object',
key: 'a',
},
{
tag: 'object',
key: 'b',
},
],
}),
)
})
})
})
})
})
})
25 changes: 18 additions & 7 deletions packages/pure-parse/src/parsers/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import {
Parser,
ParseResult,
success,
propagateFailure,
} from './types'
import { optionalSymbol } from '../internals'

const notAnObjectMsg = 'Not an object'
const propertyMissingMsg = 'Property is missing'

/**
* Same as {@link object}, but does not perform just-in-time (JIT) compilation with the `Function` constructor. This function is needed as a replacement in environments where `new Function()` is disallowed; for example, when the [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) policy is set without the `'unsafe-eval`' directive.
*/
Expand All @@ -19,7 +23,7 @@ export const objectNoJit = <T extends Record<string, unknown>>(schema: {
const entries = Object.entries(schema)
return (data) => {
if (!isObject(data)) {
return failure('Not an object')
return failure(notAnObjectMsg)
}
const dataOutput = {} as Record<string, unknown>
for (let i = 0; i < entries.length; i++) {
Expand All @@ -31,17 +35,20 @@ export const objectNoJit = <T extends Record<string, unknown>>(schema: {
// The key is optional, so we can skip it
continue
}
return failure('A property is missing')
return propagateFailure(failure(propertyMissingMsg), {
tag: 'object',
key,
})
}

const parseResult = parser(value)
if (parseResult.tag === 'failure') {
return failure('Not all properties are valid')
return propagateFailure(parseResult, { tag: 'object', key })
}
dataOutput[key] = (parseResult as ParseSuccess<unknown>).value
}

return success(dataOutput) as ParseResult<T>
return success(dataOutput as T)
}
}

Expand All @@ -65,7 +72,9 @@ export const object = <T extends Record<string, unknown>>(schema: {
const schemaEntries = Object.entries(schema)
const parsers = schemaEntries.map(([_, parser]) => parser)
const statements = [
`if(typeof data !== 'object' || data === null) return {tag:'failure', message:'Not an object'}`,
`if(typeof data !== 'object' || data === null) return {tag:'failure', error: ${JSON.stringify(
notAnObjectMsg,
)}, path: []}`,
`const dataOutput = {}`,
`let parseResult`,
...schemaEntries.flatMap(([unescapedKey, parserFunction], i) => {
Expand All @@ -78,11 +87,13 @@ export const object = <T extends Record<string, unknown>>(schema: {
return [
...(!isOptional
? [
`if(${value} === undefined && !data.hasOwnProperty(${key})) return {tag:'failure', message:'A property is missing'}`,
`if(${value} === undefined && !data.hasOwnProperty(${key})) return {tag:'failure', error:${JSON.stringify(
propertyMissingMsg,
)}, path: [{tag: 'object', key: ${key}}]}`,
]
: []),
`parseResult = ${parser}(${value})`,
`if(parseResult.tag === 'failure') return {tag:'failure', message:'Not all properties are valid'}`,
`if(parseResult.tag === 'failure') return {tag:'failure', error: parseResult.error, path:[{tag:'object', key:${key}}, ...parseResult.path]}`,
`dataOutput[${key}] = parseResult.value`,
]
}),
Expand Down
Empty file.
Loading