@badrap/valita
Advanced tools
Comparing version 0.1.5 to 0.1.6
@@ -208,7 +208,2 @@ "use strict"; | ||
class AbstractType { | ||
get func() { | ||
const f = this.genFunc(); | ||
Object.defineProperty(this, "func", { value: f }); | ||
return f; | ||
} | ||
try(v, options) { | ||
@@ -284,2 +279,5 @@ let mode = 1 /* STRICT */; | ||
class Type extends AbstractType { | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
} | ||
@@ -292,7 +290,4 @@ class Optional extends AbstractType { | ||
} | ||
genFunc() { | ||
const func = this.type.func; | ||
return (v, mode) => { | ||
return v === undefined || v === Nothing ? true : func(v, mode); | ||
}; | ||
func(v, mode) { | ||
return v === undefined || v === Nothing ? true : this.type.func(v, mode); | ||
} | ||
@@ -305,2 +300,71 @@ toTerminals(into) { | ||
} | ||
function prependIssue(issue, result) { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
function assignEnumerable(to, from) { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function addResult(objResult, obj, key, value, keyResult, assign) { | ||
if (keyResult === true) { | ||
if (objResult !== true && objResult.code === "ok" && value !== Nothing) { | ||
safeSet(objResult.value, key, value); | ||
} | ||
return objResult; | ||
} | ||
else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} | ||
else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} | ||
else { | ||
return objResult; | ||
} | ||
} | ||
else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
} | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
function createBitsetTemplate(bits) { | ||
const template = [0 | 0]; | ||
for (let i = 32; i < bits; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
return template; | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(template, bits, index) { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
} | ||
else if (index < 32) { | ||
return bits | (1 << index); | ||
} | ||
else { | ||
template[0] = bits | 0; | ||
return setBit(template, template.slice(), index); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits, index) { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
} | ||
else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
} | ||
} | ||
class ObjectType extends Type { | ||
@@ -313,20 +377,6 @@ constructor(shape, restType, checks) { | ||
this.name = "object"; | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
check(func, error) { | ||
var _a; | ||
const issue = { code: "custom_error", error }; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...((_a = this.checks) !== null && _a !== void 0 ? _a : []), | ||
{ | ||
func: func, | ||
issue, | ||
}, | ||
]); | ||
} | ||
genFunc() { | ||
const shape = this.shape; | ||
const checks = this.checks; | ||
this.invalidType = { | ||
code: "invalid_type", | ||
expected: ["object"], | ||
}; | ||
const requiredKeys = []; | ||
@@ -342,23 +392,19 @@ const optionalKeys = []; | ||
} | ||
const requiredCount = requiredKeys.length | 0; | ||
const optionalCount = optionalKeys.length | 0; | ||
const totalCount = (requiredCount + optionalCount) | 0; | ||
const keys = [...requiredKeys, ...optionalKeys]; | ||
const funcs = keys.map((key) => shape[key].func); | ||
const invertedIndexes = Object.create(null); | ||
keys.forEach((key, index) => { | ||
invertedIndexes[key] = ~index; | ||
this.requiredCount = requiredKeys.length | 0; | ||
this.optionalCount = optionalKeys.length | 0; | ||
this.totalCount = (this.requiredCount + this.optionalCount) | 0; | ||
this.keys = [...requiredKeys, ...optionalKeys]; | ||
this.types = this.keys.map((key) => shape[key]); | ||
this.bitsTemplate = createBitsetTemplate(this.totalCount); | ||
this.invertedIndexes = Object.create(null); | ||
this.keys.forEach((key, index) => { | ||
this.invertedIndexes[key] = ~index; | ||
}); | ||
const invalidType = { code: "invalid_type", expected: ["object"] }; | ||
const missingValues = requiredKeys.map((key) => ({ | ||
this.missingValues = requiredKeys.map((key) => ({ | ||
code: "missing_value", | ||
path: [key], | ||
})); | ||
function assignEnumerable(to, from) { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function assignKnown(to, from) { | ||
this.assignKnown = (to, from) => { | ||
const keys = this.keys; | ||
const requiredCount = this.requiredCount; | ||
for (let i = 0; i < keys.length; i++) { | ||
@@ -372,191 +418,151 @@ const key = keys[i]; | ||
return to; | ||
} | ||
function assignAll(to, from) { | ||
return assignKnown(assignEnumerable(to, from), from); | ||
} | ||
function addResult(objResult, func, obj, key, value, mode, assign) { | ||
const keyResult = func(value, mode); | ||
if (keyResult === true) { | ||
if (objResult !== true && | ||
objResult.code === "ok" && | ||
value !== Nothing) { | ||
safeSet(objResult.value, key, value); | ||
}; | ||
this.assignAll = (to, from) => { | ||
return this.assignKnown(assignEnumerable(to, from), from); | ||
}; | ||
} | ||
check(func, error) { | ||
var _a; | ||
const issue = { code: "custom_error", error }; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...((_a = this.checks) !== null && _a !== void 0 ? _a : []), | ||
{ | ||
func: func, | ||
issue, | ||
}, | ||
]); | ||
} | ||
checkRemainingKeys(initialResult, obj, mode, bits, assign) { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const totalCount = this.totalCount; | ||
const requiredCount = this.requiredCount; | ||
const missingValues = this.missingValues; | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} | ||
return objResult; | ||
} | ||
else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} | ||
else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} | ||
else { | ||
return objResult; | ||
result = addResult(result, obj, key, value, types[i].func(value, mode), assign); | ||
} | ||
} | ||
else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
return result; | ||
} | ||
pass(obj, mode) { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const requiredCount = this.requiredCount; | ||
const assignKnown = this.assignKnown; | ||
let result = true; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let value = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(this.missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
result = addResult(result, obj, key, value, types[i].func(value, mode), assignKnown); | ||
} | ||
function prependIssue(issue, result) { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
const template = [0 | 0]; | ||
for (let i = 32; i < keys.length; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(bits, index) { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
return result; | ||
} | ||
strict(obj, mode) { | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const assignKnown = this.assignKnown; | ||
const bitsTemplate = this.bitsTemplate; | ||
let result = true; | ||
let unrecognized = undefined; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult(result, obj, key, value, types[index].func(value, mode), assignKnown); | ||
} | ||
else if (index < 32) { | ||
return bits | (1 << index); | ||
else if (mode === 2 /* STRIP */) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} | ||
else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} | ||
else { | ||
template[0] = bits | 0; | ||
return setBit(template.slice(), index); | ||
unrecognized.push(key); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits, index) { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys(result, obj, mode, seenBits, assignKnown); | ||
} | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue({ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, result); | ||
} | ||
withRest(rest, obj, mode) { | ||
if (rest.name === "unknown" && this.totalCount === 0) { | ||
return true; | ||
} | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const bitsTemplate = this.bitsTemplate; | ||
let result = true; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult(result, obj, key, value, types[index].func(value, mode), assignEnumerable); | ||
} | ||
else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
result = addResult(result, obj, key, value, rest.func(value, mode), assignEnumerable); | ||
} | ||
} | ||
function checkRemainingKeys(initialResult, obj, mode, bits, assign) { | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} | ||
else { | ||
result = addResult(result, funcs[i], obj, key, value, mode, assign); | ||
} | ||
} | ||
} | ||
return result; | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys(result, obj, mode, seenBits, this.assignAll); | ||
} | ||
function strict(obj, mode) { | ||
let result = true; | ||
let unrecognized = undefined; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
result = addResult(result, funcs[index], obj, key, value, mode, assignKnown); | ||
return result; | ||
} | ||
runChecks(obj, result) { | ||
const checks = this.checks; | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
else if (mode === 2 /* STRIP */) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} | ||
else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} | ||
else { | ||
unrecognized.push(key); | ||
} | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignKnown); | ||
} | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue({ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, result); | ||
} | ||
function pass(obj, mode) { | ||
let result = true; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let value = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
result = addResult(result, funcs[i], obj, key, value, mode, assignKnown); | ||
} | ||
return result; | ||
return result; | ||
} | ||
func(obj, mode) { | ||
if (!isObject(obj)) { | ||
return this.invalidType; | ||
} | ||
function runChecks(obj, result) { | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
if (this.restType) { | ||
const rest = this.restType.func; | ||
if (rest.name === "unknown") { | ||
if (totalCount === 0) { | ||
return (obj, _mode) => { | ||
return isObject(obj) ? runChecks(obj, true) : invalidType; | ||
}; | ||
} | ||
return (obj, mode) => { | ||
return isObject(obj) ? runChecks(obj, pass(obj, mode)) : invalidType; | ||
}; | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
let result = true; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
result = addResult(result, funcs[index], obj, key, value, mode, assignEnumerable); | ||
} | ||
else { | ||
result = addResult(result, rest, obj, key, value, mode, assignEnumerable); | ||
} | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignAll); | ||
} | ||
return runChecks(obj, result); | ||
}; | ||
return this.runChecks(obj, this.withRest(this.restType, obj, mode)); | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
return runChecks(obj, mode === 0 /* PASS */ ? pass(obj, mode) : strict(obj, mode)); | ||
}; | ||
else if (mode === 0 /* PASS */) { | ||
return this.runChecks(obj, this.pass(obj, mode)); | ||
} | ||
else { | ||
return this.runChecks(obj, this.strict(obj, mode)); | ||
} | ||
} | ||
@@ -597,4 +603,12 @@ rest(restType) { | ||
this.head = head; | ||
this.rest = rest; | ||
this.name = "array"; | ||
this.rest = rest !== null && rest !== void 0 ? rest : never(); | ||
this.minLength = this.head.length; | ||
this.maxLength = rest ? Infinity : this.minLength; | ||
this.invalidType = { code: "invalid_type", expected: ["array"] }; | ||
this.invalidLength = { | ||
code: "invalid_length", | ||
minLength: this.minLength, | ||
maxLength: this.maxLength, | ||
}; | ||
} | ||
@@ -604,49 +618,38 @@ toTerminals(into) { | ||
} | ||
genFunc() { | ||
var _a; | ||
const headFuncs = this.head.map((t) => t.func); | ||
const restFunc = ((_a = this.rest) !== null && _a !== void 0 ? _a : never()).func; | ||
const minLength = headFuncs.length; | ||
const maxLength = this.rest ? Infinity : minLength; | ||
const invalidType = { code: "invalid_type", expected: ["array"] }; | ||
const invalidLength = { | ||
code: "invalid_length", | ||
minLength, | ||
maxLength, | ||
}; | ||
return (arr, mode) => { | ||
if (!Array.isArray(arr)) { | ||
return invalidType; | ||
} | ||
const length = arr.length; | ||
if (length < minLength || length > maxLength) { | ||
return invalidLength; | ||
} | ||
let issueTree = undefined; | ||
let output = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const func = i < minLength ? headFuncs[i] : restFunc; | ||
const r = func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value; | ||
func(arr, mode) { | ||
if (!Array.isArray(arr)) { | ||
return this.invalidType; | ||
} | ||
const length = arr.length; | ||
const minLength = this.minLength; | ||
const maxLength = this.maxLength; | ||
if (length < minLength || length > maxLength) { | ||
return this.invalidLength; | ||
} | ||
let issueTree = undefined; | ||
let output = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const type = i < minLength ? this.head[i] : this.rest; | ||
const r = type.func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
} | ||
output[i] = r.value; | ||
} | ||
else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
} | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} | ||
else if (arr === output) { | ||
return true; | ||
} | ||
else { | ||
return { code: "ok", value: output }; | ||
} | ||
}; | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} | ||
else if (arr === output) { | ||
return true; | ||
} | ||
else { | ||
return { code: "ok", value: output }; | ||
} | ||
} | ||
@@ -906,2 +909,6 @@ } | ||
this.name = "union"; | ||
const flattened = flatten(options.map((root) => ({ root, type: root }))); | ||
this.objects = createObjectMatchers(flattened); | ||
this.base = createUnionMatcher(flattened); | ||
this.hasUnknown = hasTerminal(this, "unknown"); | ||
} | ||
@@ -911,38 +918,14 @@ toTerminals(into) { | ||
} | ||
genFunc() { | ||
const flattened = flatten(this.options.map((root) => ({ root, type: root }))); | ||
const hasUnknown = hasTerminal(this, "unknown"); | ||
const objects = createObjectMatchers(flattened); | ||
const base = createUnionMatcher(flattened); | ||
return (v, mode) => { | ||
if (!hasUnknown && objects.length > 0 && isObject(v)) { | ||
const item = objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return item.matcher(v, value, mode); | ||
func(v, mode) { | ||
if (!this.hasUnknown && this.objects.length > 0 && isObject(v)) { | ||
const item = this.objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return base(v, v, mode); | ||
}; | ||
return item.matcher(v, value, mode); | ||
} | ||
return this.base(v, v, mode); | ||
} | ||
optional() { | ||
return new Optional(this); | ||
} | ||
} | ||
class LiteralType extends Type { | ||
constructor(value) { | ||
super(); | ||
this.value = value; | ||
this.name = "literal"; | ||
} | ||
genFunc() { | ||
const value = this.value; | ||
const issue = { code: "invalid_literal", expected: [value] }; | ||
return (v, _) => (v === value ? true : issue); | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
} | ||
class TransformType extends Type { | ||
@@ -954,42 +937,47 @@ constructor(transformed, transform) { | ||
this.name = "transform"; | ||
this.undef = { code: "ok", value: undefined }; | ||
this.transformChain = undefined; | ||
this.transformRoot = undefined; | ||
} | ||
genFunc() { | ||
const chain = []; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
func(v, mode) { | ||
let chain = this.transformChain; | ||
if (!chain) { | ||
chain = []; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
} | ||
chain.reverse(); | ||
this.transformChain = chain; | ||
this.transformRoot = next; | ||
} | ||
chain.reverse(); | ||
const func = next.func; | ||
const undef = { code: "ok", value: undefined }; | ||
return (v, mode) => { | ||
let result = func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
} | ||
let current; | ||
if (result !== true) { | ||
current = result.value; | ||
} | ||
else if (v === Nothing) { | ||
current = undefined; | ||
result = undef; | ||
} | ||
else { | ||
current = v; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
// eslint-disable-next-line | ||
let result = this.transformRoot.func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
} | ||
let current; | ||
if (result !== true) { | ||
current = result.value; | ||
} | ||
else if (v === Nothing) { | ||
current = undefined; | ||
result = this.undef; | ||
} | ||
else { | ||
current = v; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
} | ||
return result; | ||
}; | ||
} | ||
return result; | ||
} | ||
@@ -1005,76 +993,174 @@ toTerminals(into) { | ||
this.name = "lazy"; | ||
this.recursing = false; | ||
} | ||
get type() { | ||
const type = this.definer(); | ||
Object.defineProperty(this, "type", { value: type }); | ||
return type; | ||
func(v, mode) { | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
return this.type.func(v, mode); | ||
} | ||
genFunc() { | ||
let func = undefined; | ||
return (v, mode) => { | ||
if (!func) { | ||
func = this.type.func; | ||
} | ||
return func(v, mode); | ||
}; | ||
} | ||
toTerminals(into) { | ||
this.type.toTerminals(into); | ||
} | ||
} | ||
function singleton(name, genFunc) { | ||
class Singleton extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = name; | ||
if (this.recursing) { | ||
return; | ||
} | ||
genFunc() { | ||
return genFunc(); | ||
try { | ||
this.recursing = true; | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
this.type.toTerminals(into); | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
finally { | ||
this.recursing = false; | ||
} | ||
} | ||
const instance = new Singleton(); | ||
return () => instance; | ||
} | ||
const never = singleton("never", () => { | ||
const issue = { code: "invalid_type", expected: [] }; | ||
return (_v, _mode) => issue; | ||
}); | ||
class NeverType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "never"; | ||
this.issue = { code: "invalid_type", expected: [] }; | ||
} | ||
func(_, __) { | ||
return this.issue; | ||
} | ||
} | ||
const neverSingleton = new NeverType(); | ||
function never() { | ||
return neverSingleton; | ||
} | ||
exports.never = never; | ||
const unknown = singleton("unknown", () => { | ||
return (_v, _mode) => true; | ||
}); | ||
class UnknownType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "unknown"; | ||
} | ||
func(_, __) { | ||
return true; | ||
} | ||
} | ||
const unknownSingleton = new UnknownType(); | ||
function unknown() { | ||
return unknownSingleton; | ||
} | ||
exports.unknown = unknown; | ||
const number = singleton("number", () => { | ||
const issue = { code: "invalid_type", expected: ["number"] }; | ||
return (v, _mode) => (typeof v === "number" ? true : issue); | ||
}); | ||
class UndefinedType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "undefined"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["undefined"], | ||
}; | ||
} | ||
func(v, _) { | ||
return v === undefined ? true : this.issue; | ||
} | ||
} | ||
const undefinedSingleton = new UndefinedType(); | ||
function undefined_() { | ||
return undefinedSingleton; | ||
} | ||
exports.undefined = undefined_; | ||
class NullType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "null"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["null"], | ||
}; | ||
} | ||
func(v, _) { | ||
return v === null ? true : this.issue; | ||
} | ||
} | ||
const nullSingleton = new NullType(); | ||
function null_() { | ||
return nullSingleton; | ||
} | ||
exports.null = null_; | ||
class NumberType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "number"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["number"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "number" ? true : this.issue; | ||
} | ||
} | ||
const numberSingleton = new NumberType(); | ||
function number() { | ||
return numberSingleton; | ||
} | ||
exports.number = number; | ||
const bigint = singleton("bigint", () => { | ||
const issue = { code: "invalid_type", expected: ["bigint"] }; | ||
return (v, _mode) => (typeof v === "bigint" ? true : issue); | ||
}); | ||
class BigIntType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "bigint"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["bigint"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "bigint" ? true : this.issue; | ||
} | ||
} | ||
const bigintSingleton = new BigIntType(); | ||
function bigint() { | ||
return bigintSingleton; | ||
} | ||
exports.bigint = bigint; | ||
const string = singleton("string", () => { | ||
const issue = { code: "invalid_type", expected: ["string"] }; | ||
return (v, _mode) => (typeof v === "string" ? true : issue); | ||
}); | ||
class StringType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "string"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["string"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "string" ? true : this.issue; | ||
} | ||
} | ||
const stringSingleton = new StringType(); | ||
function string() { | ||
return stringSingleton; | ||
} | ||
exports.string = string; | ||
const boolean = singleton("boolean", () => { | ||
const issue = { code: "invalid_type", expected: ["boolean"] }; | ||
return (v, _mode) => (typeof v === "boolean" ? true : issue); | ||
}); | ||
class BooleanType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "boolean"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["boolean"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "boolean" ? true : this.issue; | ||
} | ||
} | ||
const booleanSingleton = new BooleanType(); | ||
function boolean() { | ||
return booleanSingleton; | ||
} | ||
exports.boolean = boolean; | ||
const undefined_ = singleton("undefined", () => { | ||
const issue = { code: "invalid_type", expected: ["undefined"] }; | ||
return (v, _mode) => (v === undefined ? true : issue); | ||
}); | ||
exports.undefined = undefined_; | ||
const null_ = singleton("null", () => { | ||
const issue = { code: "invalid_type", expected: ["null"] }; | ||
return (v, _mode) => (v === null ? true : issue); | ||
}); | ||
exports.null = null_; | ||
class LiteralType extends Type { | ||
constructor(value) { | ||
super(); | ||
this.value = value; | ||
this.name = "literal"; | ||
this.issue = { code: "invalid_literal", expected: [value] }; | ||
} | ||
func(v, _) { | ||
return v === this.value ? true : this.issue; | ||
} | ||
} | ||
function literal(value) { | ||
@@ -1081,0 +1167,0 @@ return new LiteralType(value); |
@@ -208,7 +208,2 @@ "use strict"; | ||
class AbstractType { | ||
get func() { | ||
const f = this.genFunc(); | ||
Object.defineProperty(this, "func", { value: f }); | ||
return f; | ||
} | ||
try(v, options) { | ||
@@ -284,2 +279,5 @@ let mode = 1 /* STRICT */; | ||
class Type extends AbstractType { | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
} | ||
@@ -292,7 +290,4 @@ class Optional extends AbstractType { | ||
} | ||
genFunc() { | ||
const func = this.type.func; | ||
return (v, mode) => { | ||
return v === undefined || v === Nothing ? true : func(v, mode); | ||
}; | ||
func(v, mode) { | ||
return v === undefined || v === Nothing ? true : this.type.func(v, mode); | ||
} | ||
@@ -305,2 +300,71 @@ toTerminals(into) { | ||
} | ||
function prependIssue(issue, result) { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
function assignEnumerable(to, from) { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function addResult(objResult, obj, key, value, keyResult, assign) { | ||
if (keyResult === true) { | ||
if (objResult !== true && objResult.code === "ok" && value !== Nothing) { | ||
safeSet(objResult.value, key, value); | ||
} | ||
return objResult; | ||
} | ||
else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} | ||
else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} | ||
else { | ||
return objResult; | ||
} | ||
} | ||
else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
} | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
function createBitsetTemplate(bits) { | ||
const template = [0 | 0]; | ||
for (let i = 32; i < bits; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
return template; | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(template, bits, index) { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
} | ||
else if (index < 32) { | ||
return bits | (1 << index); | ||
} | ||
else { | ||
template[0] = bits | 0; | ||
return setBit(template, template.slice(), index); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits, index) { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
} | ||
else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
} | ||
} | ||
class ObjectType extends Type { | ||
@@ -313,20 +377,6 @@ constructor(shape, restType, checks) { | ||
this.name = "object"; | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
check(func, error) { | ||
var _a; | ||
const issue = { code: "custom_error", error }; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...((_a = this.checks) !== null && _a !== void 0 ? _a : []), | ||
{ | ||
func: func, | ||
issue, | ||
}, | ||
]); | ||
} | ||
genFunc() { | ||
const shape = this.shape; | ||
const checks = this.checks; | ||
this.invalidType = { | ||
code: "invalid_type", | ||
expected: ["object"], | ||
}; | ||
const requiredKeys = []; | ||
@@ -342,23 +392,19 @@ const optionalKeys = []; | ||
} | ||
const requiredCount = requiredKeys.length | 0; | ||
const optionalCount = optionalKeys.length | 0; | ||
const totalCount = (requiredCount + optionalCount) | 0; | ||
const keys = [...requiredKeys, ...optionalKeys]; | ||
const funcs = keys.map((key) => shape[key].func); | ||
const invertedIndexes = Object.create(null); | ||
keys.forEach((key, index) => { | ||
invertedIndexes[key] = ~index; | ||
this.requiredCount = requiredKeys.length | 0; | ||
this.optionalCount = optionalKeys.length | 0; | ||
this.totalCount = (this.requiredCount + this.optionalCount) | 0; | ||
this.keys = [...requiredKeys, ...optionalKeys]; | ||
this.types = this.keys.map((key) => shape[key]); | ||
this.bitsTemplate = createBitsetTemplate(this.totalCount); | ||
this.invertedIndexes = Object.create(null); | ||
this.keys.forEach((key, index) => { | ||
this.invertedIndexes[key] = ~index; | ||
}); | ||
const invalidType = { code: "invalid_type", expected: ["object"] }; | ||
const missingValues = requiredKeys.map((key) => ({ | ||
this.missingValues = requiredKeys.map((key) => ({ | ||
code: "missing_value", | ||
path: [key], | ||
})); | ||
function assignEnumerable(to, from) { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function assignKnown(to, from) { | ||
this.assignKnown = (to, from) => { | ||
const keys = this.keys; | ||
const requiredCount = this.requiredCount; | ||
for (let i = 0; i < keys.length; i++) { | ||
@@ -372,191 +418,151 @@ const key = keys[i]; | ||
return to; | ||
} | ||
function assignAll(to, from) { | ||
return assignKnown(assignEnumerable(to, from), from); | ||
} | ||
function addResult(objResult, func, obj, key, value, mode, assign) { | ||
const keyResult = func(value, mode); | ||
if (keyResult === true) { | ||
if (objResult !== true && | ||
objResult.code === "ok" && | ||
value !== Nothing) { | ||
safeSet(objResult.value, key, value); | ||
}; | ||
this.assignAll = (to, from) => { | ||
return this.assignKnown(assignEnumerable(to, from), from); | ||
}; | ||
} | ||
check(func, error) { | ||
var _a; | ||
const issue = { code: "custom_error", error }; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...((_a = this.checks) !== null && _a !== void 0 ? _a : []), | ||
{ | ||
func: func, | ||
issue, | ||
}, | ||
]); | ||
} | ||
checkRemainingKeys(initialResult, obj, mode, bits, assign) { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const totalCount = this.totalCount; | ||
const requiredCount = this.requiredCount; | ||
const missingValues = this.missingValues; | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} | ||
return objResult; | ||
} | ||
else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} | ||
else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} | ||
else { | ||
return objResult; | ||
result = addResult(result, obj, key, value, types[i].func(value, mode), assign); | ||
} | ||
} | ||
else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
return result; | ||
} | ||
pass(obj, mode) { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const requiredCount = this.requiredCount; | ||
const assignKnown = this.assignKnown; | ||
let result = true; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let value = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(this.missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
result = addResult(result, obj, key, value, types[i].func(value, mode), assignKnown); | ||
} | ||
function prependIssue(issue, result) { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
const template = [0 | 0]; | ||
for (let i = 32; i < keys.length; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(bits, index) { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
return result; | ||
} | ||
strict(obj, mode) { | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const assignKnown = this.assignKnown; | ||
const bitsTemplate = this.bitsTemplate; | ||
let result = true; | ||
let unrecognized = undefined; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult(result, obj, key, value, types[index].func(value, mode), assignKnown); | ||
} | ||
else if (index < 32) { | ||
return bits | (1 << index); | ||
else if (mode === 2 /* STRIP */) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} | ||
else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} | ||
else { | ||
template[0] = bits | 0; | ||
return setBit(template.slice(), index); | ||
unrecognized.push(key); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits, index) { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys(result, obj, mode, seenBits, assignKnown); | ||
} | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue({ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, result); | ||
} | ||
withRest(rest, obj, mode) { | ||
if (rest.name === "unknown" && this.totalCount === 0) { | ||
return true; | ||
} | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const bitsTemplate = this.bitsTemplate; | ||
let result = true; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult(result, obj, key, value, types[index].func(value, mode), assignEnumerable); | ||
} | ||
else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
result = addResult(result, obj, key, value, rest.func(value, mode), assignEnumerable); | ||
} | ||
} | ||
function checkRemainingKeys(initialResult, obj, mode, bits, assign) { | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} | ||
else { | ||
result = addResult(result, funcs[i], obj, key, value, mode, assign); | ||
} | ||
} | ||
} | ||
return result; | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys(result, obj, mode, seenBits, this.assignAll); | ||
} | ||
function strict(obj, mode) { | ||
let result = true; | ||
let unrecognized = undefined; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
result = addResult(result, funcs[index], obj, key, value, mode, assignKnown); | ||
return result; | ||
} | ||
runChecks(obj, result) { | ||
const checks = this.checks; | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
else if (mode === 2 /* STRIP */) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} | ||
else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} | ||
else { | ||
unrecognized.push(key); | ||
} | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignKnown); | ||
} | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue({ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, result); | ||
} | ||
function pass(obj, mode) { | ||
let result = true; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let value = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
result = addResult(result, funcs[i], obj, key, value, mode, assignKnown); | ||
} | ||
return result; | ||
return result; | ||
} | ||
func(obj, mode) { | ||
if (!isObject(obj)) { | ||
return this.invalidType; | ||
} | ||
function runChecks(obj, result) { | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
if (this.restType) { | ||
const rest = this.restType.func; | ||
if (rest.name === "unknown") { | ||
if (totalCount === 0) { | ||
return (obj, _mode) => { | ||
return isObject(obj) ? runChecks(obj, true) : invalidType; | ||
}; | ||
} | ||
return (obj, mode) => { | ||
return isObject(obj) ? runChecks(obj, pass(obj, mode)) : invalidType; | ||
}; | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
let result = true; | ||
let seenBits = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
result = addResult(result, funcs[index], obj, key, value, mode, assignEnumerable); | ||
} | ||
else { | ||
result = addResult(result, rest, obj, key, value, mode, assignEnumerable); | ||
} | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignAll); | ||
} | ||
return runChecks(obj, result); | ||
}; | ||
return this.runChecks(obj, this.withRest(this.restType, obj, mode)); | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
return runChecks(obj, mode === 0 /* PASS */ ? pass(obj, mode) : strict(obj, mode)); | ||
}; | ||
else if (mode === 0 /* PASS */) { | ||
return this.runChecks(obj, this.pass(obj, mode)); | ||
} | ||
else { | ||
return this.runChecks(obj, this.strict(obj, mode)); | ||
} | ||
} | ||
@@ -597,4 +603,12 @@ rest(restType) { | ||
this.head = head; | ||
this.rest = rest; | ||
this.name = "array"; | ||
this.rest = rest !== null && rest !== void 0 ? rest : never(); | ||
this.minLength = this.head.length; | ||
this.maxLength = rest ? Infinity : this.minLength; | ||
this.invalidType = { code: "invalid_type", expected: ["array"] }; | ||
this.invalidLength = { | ||
code: "invalid_length", | ||
minLength: this.minLength, | ||
maxLength: this.maxLength, | ||
}; | ||
} | ||
@@ -604,49 +618,38 @@ toTerminals(into) { | ||
} | ||
genFunc() { | ||
var _a; | ||
const headFuncs = this.head.map((t) => t.func); | ||
const restFunc = ((_a = this.rest) !== null && _a !== void 0 ? _a : never()).func; | ||
const minLength = headFuncs.length; | ||
const maxLength = this.rest ? Infinity : minLength; | ||
const invalidType = { code: "invalid_type", expected: ["array"] }; | ||
const invalidLength = { | ||
code: "invalid_length", | ||
minLength, | ||
maxLength, | ||
}; | ||
return (arr, mode) => { | ||
if (!Array.isArray(arr)) { | ||
return invalidType; | ||
} | ||
const length = arr.length; | ||
if (length < minLength || length > maxLength) { | ||
return invalidLength; | ||
} | ||
let issueTree = undefined; | ||
let output = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const func = i < minLength ? headFuncs[i] : restFunc; | ||
const r = func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value; | ||
func(arr, mode) { | ||
if (!Array.isArray(arr)) { | ||
return this.invalidType; | ||
} | ||
const length = arr.length; | ||
const minLength = this.minLength; | ||
const maxLength = this.maxLength; | ||
if (length < minLength || length > maxLength) { | ||
return this.invalidLength; | ||
} | ||
let issueTree = undefined; | ||
let output = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const type = i < minLength ? this.head[i] : this.rest; | ||
const r = type.func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
} | ||
output[i] = r.value; | ||
} | ||
else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
} | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} | ||
else if (arr === output) { | ||
return true; | ||
} | ||
else { | ||
return { code: "ok", value: output }; | ||
} | ||
}; | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} | ||
else if (arr === output) { | ||
return true; | ||
} | ||
else { | ||
return { code: "ok", value: output }; | ||
} | ||
} | ||
@@ -906,2 +909,6 @@ } | ||
this.name = "union"; | ||
const flattened = flatten(options.map((root) => ({ root, type: root }))); | ||
this.objects = createObjectMatchers(flattened); | ||
this.base = createUnionMatcher(flattened); | ||
this.hasUnknown = hasTerminal(this, "unknown"); | ||
} | ||
@@ -911,38 +918,14 @@ toTerminals(into) { | ||
} | ||
genFunc() { | ||
const flattened = flatten(this.options.map((root) => ({ root, type: root }))); | ||
const hasUnknown = hasTerminal(this, "unknown"); | ||
const objects = createObjectMatchers(flattened); | ||
const base = createUnionMatcher(flattened); | ||
return (v, mode) => { | ||
if (!hasUnknown && objects.length > 0 && isObject(v)) { | ||
const item = objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return item.matcher(v, value, mode); | ||
func(v, mode) { | ||
if (!this.hasUnknown && this.objects.length > 0 && isObject(v)) { | ||
const item = this.objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return base(v, v, mode); | ||
}; | ||
return item.matcher(v, value, mode); | ||
} | ||
return this.base(v, v, mode); | ||
} | ||
optional() { | ||
return new Optional(this); | ||
} | ||
} | ||
class LiteralType extends Type { | ||
constructor(value) { | ||
super(); | ||
this.value = value; | ||
this.name = "literal"; | ||
} | ||
genFunc() { | ||
const value = this.value; | ||
const issue = { code: "invalid_literal", expected: [value] }; | ||
return (v, _) => (v === value ? true : issue); | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
} | ||
} | ||
class TransformType extends Type { | ||
@@ -954,42 +937,47 @@ constructor(transformed, transform) { | ||
this.name = "transform"; | ||
this.undef = { code: "ok", value: undefined }; | ||
this.transformChain = undefined; | ||
this.transformRoot = undefined; | ||
} | ||
genFunc() { | ||
const chain = []; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
func(v, mode) { | ||
let chain = this.transformChain; | ||
if (!chain) { | ||
chain = []; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
} | ||
chain.reverse(); | ||
this.transformChain = chain; | ||
this.transformRoot = next; | ||
} | ||
chain.reverse(); | ||
const func = next.func; | ||
const undef = { code: "ok", value: undefined }; | ||
return (v, mode) => { | ||
let result = func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
} | ||
let current; | ||
if (result !== true) { | ||
current = result.value; | ||
} | ||
else if (v === Nothing) { | ||
current = undefined; | ||
result = undef; | ||
} | ||
else { | ||
current = v; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
// eslint-disable-next-line | ||
let result = this.transformRoot.func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
} | ||
let current; | ||
if (result !== true) { | ||
current = result.value; | ||
} | ||
else if (v === Nothing) { | ||
current = undefined; | ||
result = this.undef; | ||
} | ||
else { | ||
current = v; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
} | ||
return result; | ||
}; | ||
} | ||
return result; | ||
} | ||
@@ -1005,76 +993,174 @@ toTerminals(into) { | ||
this.name = "lazy"; | ||
this.recursing = false; | ||
} | ||
get type() { | ||
const type = this.definer(); | ||
Object.defineProperty(this, "type", { value: type }); | ||
return type; | ||
func(v, mode) { | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
return this.type.func(v, mode); | ||
} | ||
genFunc() { | ||
let func = undefined; | ||
return (v, mode) => { | ||
if (!func) { | ||
func = this.type.func; | ||
} | ||
return func(v, mode); | ||
}; | ||
} | ||
toTerminals(into) { | ||
this.type.toTerminals(into); | ||
} | ||
} | ||
function singleton(name, genFunc) { | ||
class Singleton extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = name; | ||
if (this.recursing) { | ||
return; | ||
} | ||
genFunc() { | ||
return genFunc(); | ||
try { | ||
this.recursing = true; | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
this.type.toTerminals(into); | ||
} | ||
toTerminals(into) { | ||
into.push(this); | ||
finally { | ||
this.recursing = false; | ||
} | ||
} | ||
const instance = new Singleton(); | ||
return () => instance; | ||
} | ||
const never = singleton("never", () => { | ||
const issue = { code: "invalid_type", expected: [] }; | ||
return (_v, _mode) => issue; | ||
}); | ||
class NeverType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "never"; | ||
this.issue = { code: "invalid_type", expected: [] }; | ||
} | ||
func(_, __) { | ||
return this.issue; | ||
} | ||
} | ||
const neverSingleton = new NeverType(); | ||
function never() { | ||
return neverSingleton; | ||
} | ||
exports.never = never; | ||
const unknown = singleton("unknown", () => { | ||
return (_v, _mode) => true; | ||
}); | ||
class UnknownType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "unknown"; | ||
} | ||
func(_, __) { | ||
return true; | ||
} | ||
} | ||
const unknownSingleton = new UnknownType(); | ||
function unknown() { | ||
return unknownSingleton; | ||
} | ||
exports.unknown = unknown; | ||
const number = singleton("number", () => { | ||
const issue = { code: "invalid_type", expected: ["number"] }; | ||
return (v, _mode) => (typeof v === "number" ? true : issue); | ||
}); | ||
class UndefinedType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "undefined"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["undefined"], | ||
}; | ||
} | ||
func(v, _) { | ||
return v === undefined ? true : this.issue; | ||
} | ||
} | ||
const undefinedSingleton = new UndefinedType(); | ||
function undefined_() { | ||
return undefinedSingleton; | ||
} | ||
exports.undefined = undefined_; | ||
class NullType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "null"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["null"], | ||
}; | ||
} | ||
func(v, _) { | ||
return v === null ? true : this.issue; | ||
} | ||
} | ||
const nullSingleton = new NullType(); | ||
function null_() { | ||
return nullSingleton; | ||
} | ||
exports.null = null_; | ||
class NumberType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "number"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["number"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "number" ? true : this.issue; | ||
} | ||
} | ||
const numberSingleton = new NumberType(); | ||
function number() { | ||
return numberSingleton; | ||
} | ||
exports.number = number; | ||
const bigint = singleton("bigint", () => { | ||
const issue = { code: "invalid_type", expected: ["bigint"] }; | ||
return (v, _mode) => (typeof v === "bigint" ? true : issue); | ||
}); | ||
class BigIntType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "bigint"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["bigint"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "bigint" ? true : this.issue; | ||
} | ||
} | ||
const bigintSingleton = new BigIntType(); | ||
function bigint() { | ||
return bigintSingleton; | ||
} | ||
exports.bigint = bigint; | ||
const string = singleton("string", () => { | ||
const issue = { code: "invalid_type", expected: ["string"] }; | ||
return (v, _mode) => (typeof v === "string" ? true : issue); | ||
}); | ||
class StringType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "string"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["string"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "string" ? true : this.issue; | ||
} | ||
} | ||
const stringSingleton = new StringType(); | ||
function string() { | ||
return stringSingleton; | ||
} | ||
exports.string = string; | ||
const boolean = singleton("boolean", () => { | ||
const issue = { code: "invalid_type", expected: ["boolean"] }; | ||
return (v, _mode) => (typeof v === "boolean" ? true : issue); | ||
}); | ||
class BooleanType extends Type { | ||
constructor() { | ||
super(...arguments); | ||
this.name = "boolean"; | ||
this.issue = { | ||
code: "invalid_type", | ||
expected: ["boolean"], | ||
}; | ||
} | ||
func(v, _) { | ||
return typeof v === "boolean" ? true : this.issue; | ||
} | ||
} | ||
const booleanSingleton = new BooleanType(); | ||
function boolean() { | ||
return booleanSingleton; | ||
} | ||
exports.boolean = boolean; | ||
const undefined_ = singleton("undefined", () => { | ||
const issue = { code: "invalid_type", expected: ["undefined"] }; | ||
return (v, _mode) => (v === undefined ? true : issue); | ||
}); | ||
exports.undefined = undefined_; | ||
const null_ = singleton("null", () => { | ||
const issue = { code: "invalid_type", expected: ["null"] }; | ||
return (v, _mode) => (v === null ? true : issue); | ||
}); | ||
exports.null = null_; | ||
class LiteralType extends Type { | ||
constructor(value) { | ||
super(); | ||
this.value = value; | ||
this.name = "literal"; | ||
this.issue = { code: "invalid_literal", expected: [value] }; | ||
} | ||
func(v, _) { | ||
return v === this.value ? true : this.issue; | ||
} | ||
} | ||
function literal(value) { | ||
@@ -1081,0 +1167,0 @@ return new LiteralType(value); |
@@ -68,3 +68,2 @@ declare type PrettyIntersection<V> = Extract<{ | ||
} | ||
declare type Func<T> = (v: unknown, mode: FuncMode) => RawResult<T>; | ||
declare type ParseOptions = { | ||
@@ -76,5 +75,4 @@ mode: "passthrough" | "strict" | "strip"; | ||
abstract readonly name: string; | ||
abstract genFunc(): Func<Output>; | ||
abstract toTerminals(into: TerminalType[]): void; | ||
get func(): Func<Output>; | ||
abstract func(v: unknown, mode: FuncMode): RawResult<Output>; | ||
try<T extends AbstractType>(this: T, v: unknown, options?: Partial<ParseOptions>): ValitaResult<Infer<T>>; | ||
@@ -95,2 +93,3 @@ parse<T extends AbstractType>(this: T, v: unknown, options?: Partial<ParseOptions>): Infer<T>; | ||
protected readonly [isOptional] = false; | ||
toTerminals(into: TerminalType[]): void; | ||
} | ||
@@ -102,3 +101,3 @@ declare class Optional<Output = unknown> extends AbstractType<Output | undefined> { | ||
constructor(type: AbstractType<Output>); | ||
genFunc(): Func<Output | undefined>; | ||
func(v: unknown, mode: FuncMode): RawResult<Output | undefined>; | ||
toTerminals(into: TerminalType[]): void; | ||
@@ -119,2 +118,3 @@ } | ||
}> : unknown)>; | ||
declare type Obj = Record<string, unknown>; | ||
declare class ObjectType<Shape extends ObjectShape = ObjectShape, Rest extends AbstractType | undefined = AbstractType | undefined> extends Type<ObjectOutput<Shape, Rest>> { | ||
@@ -125,2 +125,13 @@ readonly shape: Shape; | ||
readonly name = "object"; | ||
private readonly missingValues; | ||
private readonly invalidType; | ||
private readonly keys; | ||
private readonly types; | ||
private readonly requiredCount; | ||
private readonly optionalCount; | ||
private readonly totalCount; | ||
private readonly bitsTemplate; | ||
private readonly invertedIndexes; | ||
private readonly assignKnown; | ||
private readonly assignAll; | ||
constructor(shape: Shape, restType: Rest, checks?: { | ||
@@ -130,5 +141,9 @@ func: (v: unknown) => boolean; | ||
}[] | undefined); | ||
toTerminals(into: TerminalType[]): void; | ||
check(func: (v: ObjectOutput<Shape, Rest>) => boolean, error?: CustomError): ObjectType<Shape, Rest>; | ||
genFunc(): Func<ObjectOutput<Shape, Rest>>; | ||
private checkRemainingKeys; | ||
private pass; | ||
private strict; | ||
private withRest; | ||
private runChecks; | ||
func(obj: Obj, mode: FuncMode): RawResult<ObjectOutput<Shape, Rest>>; | ||
rest<R extends Type>(restType: R): ObjectType<Shape, R>; | ||
@@ -151,7 +166,11 @@ extend<S extends ObjectShape>(shape: S): ObjectType<Omit<Shape, keyof S> & S, Rest>; | ||
readonly head: Head; | ||
readonly rest?: Rest | undefined; | ||
readonly name = "array"; | ||
constructor(head: Head, rest?: Rest | undefined); | ||
private readonly rest; | ||
private readonly invalidType; | ||
private readonly invalidLength; | ||
private readonly minLength; | ||
private readonly maxLength; | ||
constructor(head: Head, rest?: Rest); | ||
toTerminals(into: TerminalType[]): void; | ||
genFunc(): Func<ArrayOutput<Head, Rest>>; | ||
func(arr: unknown, mode: FuncMode): RawResult<ArrayOutput<Head, Rest>>; | ||
} | ||
@@ -161,38 +180,63 @@ declare class UnionType<T extends Type[] = Type[]> extends Type<Infer<T[number]>> { | ||
readonly name = "union"; | ||
private readonly hasUnknown; | ||
private readonly objects; | ||
private readonly base; | ||
constructor(options: T); | ||
toTerminals(into: TerminalType[]): void; | ||
genFunc(): Func<Infer<T[number]>>; | ||
optional(): Optional<Infer<T[number]>>; | ||
func(v: unknown, mode: FuncMode): RawResult<Infer<T[number]>>; | ||
} | ||
declare class NeverType extends Type<never> { | ||
readonly name = "never"; | ||
private readonly issue; | ||
func(_: unknown, __: FuncMode): RawResult<never>; | ||
} | ||
declare function never(): NeverType; | ||
declare class UnknownType extends Type<unknown> { | ||
readonly name = "unknown"; | ||
func(_: unknown, __: FuncMode): RawResult<unknown>; | ||
} | ||
declare function unknown(): UnknownType; | ||
declare class UndefinedType extends Type<undefined> { | ||
readonly name = "undefined"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<undefined>; | ||
} | ||
declare function undefined_(): UndefinedType; | ||
declare class NullType extends Type<null> { | ||
readonly name = "null"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<null>; | ||
} | ||
declare function null_(): NullType; | ||
declare class NumberType extends Type<number> { | ||
readonly name = "number"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<number>; | ||
} | ||
declare function number(): NumberType; | ||
declare class BigIntType extends Type<bigint> { | ||
readonly name = "bigint"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<bigint>; | ||
} | ||
declare function bigint(): BigIntType; | ||
declare class StringType extends Type<string> { | ||
readonly name = "string"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<string>; | ||
} | ||
declare function string(): StringType; | ||
declare class BooleanType extends Type<boolean> { | ||
readonly name = "boolean"; | ||
private readonly issue; | ||
func(v: unknown, _: FuncMode): RawResult<boolean>; | ||
} | ||
declare function boolean(): BooleanType; | ||
declare class LiteralType<Out extends Literal = Literal> extends Type<Out> { | ||
readonly value: Out; | ||
readonly name = "literal"; | ||
private readonly issue; | ||
constructor(value: Out); | ||
genFunc(): Func<Out>; | ||
toTerminals(into: TerminalType[]): void; | ||
func(v: unknown, _: FuncMode): RawResult<Out>; | ||
} | ||
declare const never: () => Type<never> & { | ||
name: "never"; | ||
}; | ||
declare const unknown: () => Type<unknown> & { | ||
name: "unknown"; | ||
}; | ||
declare const number: () => Type<number> & { | ||
name: "number"; | ||
}; | ||
declare const bigint: () => Type<bigint> & { | ||
name: "bigint"; | ||
}; | ||
declare const string: () => Type<string> & { | ||
name: "string"; | ||
}; | ||
declare const boolean: () => Type<boolean> & { | ||
name: "boolean"; | ||
}; | ||
declare const undefined_: () => Type<undefined> & { | ||
name: "undefined"; | ||
}; | ||
declare const null_: () => Type<null> & { | ||
name: "null"; | ||
}; | ||
declare function literal<T extends Literal>(value: T): Type<T>; | ||
@@ -199,0 +243,0 @@ declare function object<T extends Record<string, Type | Optional>>(obj: T): ObjectType<T, undefined>; |
{ | ||
"name": "@badrap/valita", | ||
"version": "0.1.5", | ||
"version": "0.1.6", | ||
"description": "A validation & parsing library for TypeScript", | ||
@@ -5,0 +5,0 @@ "main": "./dist/cjs/index.js", |
1045
src/index.ts
@@ -295,11 +295,5 @@ // This is magic that turns object intersections to nicer-looking types. | ||
abstract readonly name: string; | ||
abstract genFunc(): Func<Output>; | ||
abstract toTerminals(into: TerminalType[]): void; | ||
abstract func(v: unknown, mode: FuncMode): RawResult<Output>; | ||
get func(): Func<Output> { | ||
const f = this.genFunc(); | ||
Object.defineProperty(this, "func", { value: f }); | ||
return f; | ||
} | ||
try<T extends AbstractType>( | ||
@@ -404,2 +398,6 @@ this: T, | ||
protected declare readonly [isOptional] = false; | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this as TerminalType); | ||
} | ||
} | ||
@@ -414,7 +412,4 @@ | ||
} | ||
genFunc(): Func<Output | undefined> { | ||
const func = this.type.func; | ||
return (v, mode) => { | ||
return v === undefined || v === Nothing ? true : func(v, mode); | ||
}; | ||
func(v: unknown, mode: FuncMode): RawResult<Output | undefined> { | ||
return v === undefined || v === Nothing ? true : this.type.func(v, mode); | ||
} | ||
@@ -449,2 +444,93 @@ toTerminals(into: TerminalType[]): void { | ||
function prependIssue(issue: IssueTree, result: RawResult<unknown>): IssueTree { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
type Obj = Record<string, unknown>; | ||
type AssignFunc = (to: Obj, from: Obj) => Obj; | ||
function assignEnumerable(to: Obj, from: Obj): Obj { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function addResult( | ||
objResult: RawResult<Obj>, | ||
obj: Obj, | ||
key: string, | ||
value: unknown, | ||
keyResult: RawResult<unknown>, | ||
assign: AssignFunc | ||
): RawResult<Obj> { | ||
if (keyResult === true) { | ||
if (objResult !== true && objResult.code === "ok" && value !== Nothing) { | ||
safeSet(objResult.value, key, value); | ||
} | ||
return objResult; | ||
} else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} else { | ||
return objResult; | ||
} | ||
} else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
} | ||
// A bitset type, used for keeping track which known (required & optional) keys | ||
// the object validator has seen. Basically, when key `knownKey` is encountered, | ||
// the corresponding bit at index `keys.indexOf(knownKey)` gets flipped to 1. | ||
// | ||
// BitSet values initially start as a number (to avoid garbage collector churn), | ||
// and an empty BitSet is initialized like this: | ||
// let bitSet: BitSet = 0; | ||
// | ||
// As JavaScript bit arithmetic for numbers can only deal with 32-bit numbers, | ||
// BitSet values are upgraded to number arrays if a bits other than 0-31 need | ||
// to be flipped. | ||
type BitSet = number | number[]; | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
function createBitsetTemplate(bits: number): number[] { | ||
const template = [0 | 0]; | ||
for (let i = 32; i < bits; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
return template; | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(template: number[], bits: BitSet, index: number): BitSet { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
} else if (index < 32) { | ||
return bits | (1 << index); | ||
} else { | ||
template[0] = bits | 0; | ||
return setBit(template, template.slice(), index); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits: BitSet, index: number): number { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
} else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
} | ||
} | ||
class ObjectType< | ||
@@ -456,2 +542,18 @@ Shape extends ObjectShape = ObjectShape, | ||
private readonly missingValues: Issue[]; | ||
private readonly invalidType: Issue = { | ||
code: "invalid_type", | ||
expected: ["object"], | ||
}; | ||
private readonly keys: string[]; | ||
private readonly types: AbstractType[]; | ||
private readonly requiredCount: number; | ||
private readonly optionalCount: number; | ||
private readonly totalCount: number; | ||
private readonly bitsTemplate: number[]; | ||
private readonly invertedIndexes: Record<string, number>; | ||
private readonly assignKnown: AssignFunc; | ||
private readonly assignAll: AssignFunc; | ||
constructor( | ||
@@ -466,27 +568,3 @@ readonly shape: Shape, | ||
super(); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this as ObjectType); | ||
} | ||
check( | ||
func: (v: ObjectOutput<Shape, Rest>) => boolean, | ||
error?: CustomError | ||
): ObjectType<Shape, Rest> { | ||
const issue = { code: "custom_error", error } as const; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...(this.checks ?? []), | ||
{ | ||
func: func as (v: unknown) => boolean, | ||
issue, | ||
}, | ||
]); | ||
} | ||
genFunc(): Func<ObjectOutput<Shape, Rest>> { | ||
type Obj = Record<string, unknown>; | ||
const shape = this.shape; | ||
const checks = this.checks; | ||
const requiredKeys: string[] = []; | ||
@@ -502,15 +580,16 @@ const optionalKeys: string[] = []; | ||
const requiredCount = requiredKeys.length | 0; | ||
const optionalCount = optionalKeys.length | 0; | ||
const totalCount = (requiredCount + optionalCount) | 0; | ||
this.requiredCount = requiredKeys.length | 0; | ||
this.optionalCount = optionalKeys.length | 0; | ||
this.totalCount = (this.requiredCount + this.optionalCount) | 0; | ||
const keys = [...requiredKeys, ...optionalKeys]; | ||
const funcs = keys.map((key) => shape[key].func); | ||
const invertedIndexes: Record<string, number> = Object.create(null); | ||
keys.forEach((key, index) => { | ||
invertedIndexes[key] = ~index; | ||
this.keys = [...requiredKeys, ...optionalKeys]; | ||
this.types = this.keys.map((key) => shape[key]); | ||
this.bitsTemplate = createBitsetTemplate(this.totalCount); | ||
this.invertedIndexes = Object.create(null); | ||
this.keys.forEach((key, index) => { | ||
this.invertedIndexes[key] = ~index; | ||
}); | ||
const invalidType: Issue = { code: "invalid_type", expected: ["object"] }; | ||
const missingValues: Issue[] = requiredKeys.map((key) => ({ | ||
this.missingValues = requiredKeys.map((key) => ({ | ||
code: "missing_value", | ||
@@ -520,10 +599,5 @@ path: [key], | ||
function assignEnumerable(to: Obj, from: Obj): Obj { | ||
for (const key in from) { | ||
safeSet(to, key, from[key]); | ||
} | ||
return to; | ||
} | ||
function assignKnown(to: Obj, from: Obj): Obj { | ||
this.assignKnown = (to, from) => { | ||
const keys = this.keys; | ||
const requiredCount = this.requiredCount; | ||
for (let i = 0; i < keys.length; i++) { | ||
@@ -537,277 +611,232 @@ const key = keys[i]; | ||
return to; | ||
} | ||
}; | ||
function assignAll(to: Obj, from: Obj): Obj { | ||
return assignKnown(assignEnumerable(to, from), from); | ||
} | ||
this.assignAll = (to, from) => { | ||
return this.assignKnown(assignEnumerable(to, from), from); | ||
}; | ||
} | ||
function addResult( | ||
objResult: RawResult<Obj>, | ||
func: Func<unknown>, | ||
obj: Obj, | ||
key: string, | ||
value: unknown, | ||
mode: FuncMode, | ||
assign: (to: Obj, from: Obj) => Obj | ||
): RawResult<Obj> { | ||
const keyResult = func(value, mode); | ||
if (keyResult === true) { | ||
if ( | ||
objResult !== true && | ||
objResult.code === "ok" && | ||
value !== Nothing | ||
) { | ||
safeSet(objResult.value, key, value); | ||
} | ||
return objResult; | ||
} else if (keyResult.code === "ok") { | ||
if (objResult === true) { | ||
const copy = assign({}, obj); | ||
safeSet(copy, key, keyResult.value); | ||
return { code: "ok", value: copy }; | ||
} else if (objResult.code === "ok") { | ||
safeSet(objResult.value, key, keyResult.value); | ||
return objResult; | ||
} else { | ||
return objResult; | ||
} | ||
} else { | ||
return prependIssue(prependPath(key, keyResult), objResult); | ||
} | ||
} | ||
check( | ||
func: (v: ObjectOutput<Shape, Rest>) => boolean, | ||
error?: CustomError | ||
): ObjectType<Shape, Rest> { | ||
const issue = { code: "custom_error", error } as const; | ||
return new ObjectType(this.shape, this.restType, [ | ||
...(this.checks ?? []), | ||
{ | ||
func: func as (v: unknown) => boolean, | ||
issue, | ||
}, | ||
]); | ||
} | ||
function prependIssue( | ||
issue: IssueTree, | ||
result: RawResult<unknown> | ||
): IssueTree { | ||
return result === true || result.code === "ok" | ||
? issue | ||
: joinIssues(issue, result); | ||
} | ||
private checkRemainingKeys( | ||
initialResult: RawResult<Obj>, | ||
obj: Obj, | ||
mode: FuncMode, | ||
bits: BitSet, | ||
assign: (to: Obj, from: Obj) => Obj | ||
): RawResult<Obj> { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const totalCount = this.totalCount; | ||
const requiredCount = this.requiredCount; | ||
const missingValues = this.missingValues; | ||
// A bitset type, used for keeping track which known (required & optional) keys | ||
// the object validator has seen. Basically, when key `knownKey` is encountered, | ||
// the corresponding bit at index `keys.indexOf(knownKey)` gets flipped to 1. | ||
// | ||
// BitSet values initially start as a number (to avoid garbage collector churn), | ||
// and an empty BitSet is initialized like this: | ||
// let bitSet: BitSet = 0; | ||
// | ||
// As JavaScript bit arithmetic for numbers can only deal with 32-bit numbers, | ||
// BitSet values are upgraded to number arrays if a bits other than 0-31 need | ||
// to be flipped. | ||
type BitSet = number | number[]; | ||
// Preallocate a "template" array for fast cloning, in case the BitSet needs to | ||
// be upgraded to an array. This will only become useful when keys.length > 32. | ||
const template = [0 | 0]; | ||
for (let i = 32; i < keys.length; i += 32) { | ||
template.push(0 | 0); | ||
} | ||
// Set a bit in position `index` to one and return the updated bitset. | ||
// This function may or may not mutate `bits` in-place. | ||
function setBit(bits: BitSet, index: number): BitSet { | ||
if (typeof bits !== "number") { | ||
bits[index >> 5] |= 1 << index % 32; | ||
return bits; | ||
} else if (index < 32) { | ||
return bits | (1 << index); | ||
} else { | ||
template[0] = bits | 0; | ||
return setBit(template.slice(), index); | ||
} | ||
} | ||
// Get the bit at position `index`. | ||
function getBit(bits: BitSet, index: number): number { | ||
if (typeof bits === "number") { | ||
return index < 32 ? (bits >>> index) & 1 : 0; | ||
} else { | ||
return (bits[index >> 5] >>> index % 32) & 1; | ||
} | ||
} | ||
function checkRemainingKeys( | ||
initialResult: RawResult<Obj>, | ||
obj: Obj, | ||
mode: FuncMode, | ||
bits: BitSet, | ||
assign: (to: Obj, from: Obj) => Obj | ||
): RawResult<Obj> { | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} else { | ||
result = addResult(result, funcs[i], obj, key, value, mode, assign); | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
function strict(obj: Obj, mode: FuncMode): RawResult<Obj> { | ||
let result: RawResult<Obj> = true; | ||
let unrecognized: Key[] | undefined = undefined; | ||
let seenBits: BitSet = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
let result = initialResult; | ||
for (let i = 0; i < totalCount; i++) { | ||
if (!getBit(bits, i)) { | ||
const key = keys[i]; | ||
const value = key in obj ? obj[key] : Nothing; | ||
if (i < requiredCount && value === Nothing) { | ||
result = prependIssue(missingValues[i], result); | ||
} else { | ||
result = addResult( | ||
result, | ||
funcs[index], | ||
obj, | ||
key, | ||
value, | ||
mode, | ||
assignKnown | ||
types[i].func(value, mode), | ||
assign | ||
); | ||
} else if (mode === FuncMode.STRIP) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} else { | ||
unrecognized.push(key); | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignKnown); | ||
private pass(obj: Obj, mode: FuncMode): RawResult<Obj> { | ||
const keys = this.keys; | ||
const types = this.types; | ||
const requiredCount = this.requiredCount; | ||
const assignKnown = this.assignKnown; | ||
let result: RawResult<Obj> = true; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let value: unknown = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(this.missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue( | ||
{ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, | ||
result | ||
); | ||
result = addResult( | ||
result, | ||
obj, | ||
key, | ||
value, | ||
types[i].func(value, mode), | ||
assignKnown | ||
); | ||
} | ||
return result; | ||
} | ||
function pass(obj: Obj, mode: FuncMode): RawResult<Obj> { | ||
let result: RawResult<Obj> = true; | ||
private strict(obj: Obj, mode: FuncMode): RawResult<Obj> { | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const assignKnown = this.assignKnown; | ||
const bitsTemplate = this.bitsTemplate; | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
let result: RawResult<Obj> = true; | ||
let unrecognized: Key[] | undefined = undefined; | ||
let seenBits: BitSet = 0; | ||
let seenCount = 0; | ||
let value: unknown = obj[key]; | ||
if (value === undefined && !(key in obj)) { | ||
if (i < requiredCount) { | ||
result = prependIssue(missingValues[i], result); | ||
continue; | ||
} | ||
value = Nothing; | ||
} | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult( | ||
result, | ||
funcs[i], | ||
obj, | ||
key, | ||
value, | ||
mode, | ||
types[index].func(value, mode), | ||
assignKnown | ||
); | ||
} else if (mode === FuncMode.STRIP) { | ||
result = | ||
result === true | ||
? { code: "ok", value: assignKnown({}, obj) } | ||
: result; | ||
} else if (unrecognized === undefined) { | ||
unrecognized = [key]; | ||
} else { | ||
unrecognized.push(key); | ||
} | ||
} | ||
return result; | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys( | ||
result, | ||
obj, | ||
mode, | ||
seenBits, | ||
assignKnown | ||
); | ||
} | ||
function runChecks( | ||
obj: Record<string, unknown>, | ||
result: RawResult<Obj> | ||
): RawResult<ObjectOutput<Shape, Rest>> { | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
} | ||
} | ||
return result as RawResult<ObjectOutput<Shape, Rest>>; | ||
return unrecognized === undefined | ||
? result | ||
: prependIssue( | ||
{ | ||
code: "unrecognized_keys", | ||
keys: unrecognized, | ||
}, | ||
result | ||
); | ||
} | ||
private withRest( | ||
rest: AbstractType, | ||
obj: Obj, | ||
mode: FuncMode | ||
): RawResult<Obj> { | ||
if (rest.name === "unknown" && this.totalCount === 0) { | ||
return true; | ||
} | ||
if (this.restType) { | ||
const rest = this.restType.func; | ||
if (rest.name === "unknown") { | ||
if (totalCount === 0) { | ||
return (obj, _mode) => { | ||
return isObject(obj) ? runChecks(obj, true) : invalidType; | ||
}; | ||
} | ||
return (obj, mode) => { | ||
return isObject(obj) ? runChecks(obj, pass(obj, mode)) : invalidType; | ||
}; | ||
} | ||
const types = this.types; | ||
const invertedIndexes = this.invertedIndexes; | ||
const bitsTemplate = this.bitsTemplate; | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
let result: RawResult<Obj> = true; | ||
let seenBits: BitSet = 0; | ||
let seenCount = 0; | ||
let result: RawResult<Obj> = true; | ||
let seenBits: BitSet = 0; | ||
let seenCount = 0; | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(bitsTemplate, seenBits, index); | ||
result = addResult( | ||
result, | ||
obj, | ||
key, | ||
value, | ||
types[index].func(value, mode), | ||
assignEnumerable | ||
); | ||
} else { | ||
result = addResult( | ||
result, | ||
obj, | ||
key, | ||
value, | ||
rest.func(value, mode), | ||
assignEnumerable | ||
); | ||
} | ||
} | ||
for (const key in obj) { | ||
const value = obj[key]; | ||
const index = ~invertedIndexes[key]; | ||
if (index >= 0) { | ||
seenCount++; | ||
seenBits = setBit(seenBits, index); | ||
result = addResult( | ||
result, | ||
funcs[index], | ||
obj, | ||
key, | ||
value, | ||
mode, | ||
assignEnumerable | ||
); | ||
} else { | ||
result = addResult( | ||
result, | ||
rest, | ||
obj, | ||
key, | ||
value, | ||
mode, | ||
assignEnumerable | ||
); | ||
} | ||
} | ||
if (seenCount < this.totalCount) { | ||
result = this.checkRemainingKeys( | ||
result, | ||
obj, | ||
mode, | ||
seenBits, | ||
this.assignAll | ||
); | ||
} | ||
return result; | ||
} | ||
if (seenCount < totalCount) { | ||
result = checkRemainingKeys(result, obj, mode, seenBits, assignAll); | ||
private runChecks( | ||
obj: Record<string, unknown>, | ||
result: RawResult<Obj> | ||
): RawResult<ObjectOutput<Shape, Rest>> { | ||
const checks = this.checks; | ||
if ((result === true || result.code === "ok") && checks) { | ||
const value = result === true ? obj : result.value; | ||
for (let i = 0; i < checks.length; i++) { | ||
if (!checks[i].func(value)) { | ||
return checks[i].issue; | ||
} | ||
} | ||
} | ||
return result as RawResult<ObjectOutput<Shape, Rest>>; | ||
} | ||
return runChecks(obj, result); | ||
}; | ||
func(obj: Obj, mode: FuncMode): RawResult<ObjectOutput<Shape, Rest>> { | ||
if (!isObject(obj)) { | ||
return this.invalidType; | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return invalidType; | ||
} | ||
return runChecks( | ||
obj, | ||
mode === FuncMode.PASS ? pass(obj, mode) : strict(obj, mode) | ||
); | ||
}; | ||
if (this.restType) { | ||
return this.runChecks(obj, this.withRest(this.restType, obj, mode)); | ||
} else if (mode === FuncMode.PASS) { | ||
return this.runChecks(obj, this.pass(obj, mode)); | ||
} else { | ||
return this.runChecks(obj, this.strict(obj, mode)); | ||
} | ||
} | ||
rest<R extends Type>(restType: R): ObjectType<Shape, R> { | ||
@@ -873,4 +902,20 @@ return new ObjectType(this.shape, restType); | ||
constructor(readonly head: Head, readonly rest?: Rest) { | ||
private readonly rest: Type; | ||
private readonly invalidType: Issue; | ||
private readonly invalidLength: Issue; | ||
private readonly minLength: number; | ||
private readonly maxLength: number; | ||
constructor(readonly head: Head, rest?: Rest) { | ||
super(); | ||
this.rest = rest ?? never(); | ||
this.minLength = this.head.length; | ||
this.maxLength = rest ? Infinity : this.minLength; | ||
this.invalidType = { code: "invalid_type", expected: ["array"] }; | ||
this.invalidLength = { | ||
code: "invalid_length", | ||
minLength: this.minLength, | ||
maxLength: this.maxLength, | ||
}; | ||
} | ||
@@ -882,48 +927,37 @@ | ||
genFunc(): Func<ArrayOutput<Head, Rest>> { | ||
const headFuncs = this.head.map((t) => t.func); | ||
const restFunc = (this.rest ?? never()).func; | ||
const minLength = headFuncs.length; | ||
const maxLength = this.rest ? Infinity : minLength; | ||
func(arr: unknown, mode: FuncMode): RawResult<ArrayOutput<Head, Rest>> { | ||
if (!Array.isArray(arr)) { | ||
return this.invalidType; | ||
} | ||
const invalidType: Issue = { code: "invalid_type", expected: ["array"] }; | ||
const invalidLength: Issue = { | ||
code: "invalid_length", | ||
minLength, | ||
maxLength, | ||
}; | ||
const length = arr.length; | ||
const minLength = this.minLength; | ||
const maxLength = this.maxLength; | ||
if (length < minLength || length > maxLength) { | ||
return this.invalidLength; | ||
} | ||
return (arr, mode) => { | ||
if (!Array.isArray(arr)) { | ||
return invalidType; | ||
} | ||
const length = arr.length; | ||
if (length < minLength || length > maxLength) { | ||
return invalidLength; | ||
} | ||
let issueTree: IssueTree | undefined = undefined; | ||
let output: unknown[] = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const func = i < minLength ? headFuncs[i] : restFunc; | ||
const r = func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value; | ||
} else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
let issueTree: IssueTree | undefined = undefined; | ||
let output: unknown[] = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const type = i < minLength ? this.head[i] : this.rest; | ||
const r = type.func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value; | ||
} else { | ||
issueTree = joinIssues(issueTree, prependPath(i, r)); | ||
} | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} else if (arr === output) { | ||
return true; | ||
} else { | ||
return { code: "ok", value: output as ArrayOutput<Head, Rest> }; | ||
} | ||
}; | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} else if (arr === output) { | ||
return true; | ||
} else { | ||
return { code: "ok", value: output as ArrayOutput<Head, Rest> }; | ||
} | ||
} | ||
@@ -1204,4 +1238,25 @@ } | ||
private readonly hasUnknown: boolean; | ||
private readonly objects: { | ||
key: string; | ||
optional?: AbstractType<unknown> | undefined; | ||
matcher: ( | ||
rootValue: unknown, | ||
value: unknown, | ||
mode: FuncMode | ||
) => RawResult<unknown>; | ||
}[]; | ||
private readonly base: ( | ||
rootValue: unknown, | ||
value: unknown, | ||
mode: FuncMode | ||
) => RawResult<unknown>; | ||
constructor(readonly options: T) { | ||
super(); | ||
const flattened = flatten(options.map((root) => ({ root, type: root }))); | ||
this.objects = createObjectMatchers(flattened); | ||
this.base = createUnionMatcher(flattened); | ||
this.hasUnknown = hasTerminal(this, "unknown"); | ||
} | ||
@@ -1213,43 +1268,22 @@ | ||
genFunc(): Func<Infer<T[number]>> { | ||
const flattened = flatten( | ||
this.options.map((root) => ({ root, type: root })) | ||
); | ||
const hasUnknown = hasTerminal(this, "unknown"); | ||
const objects = createObjectMatchers(flattened); | ||
const base = createUnionMatcher(flattened); | ||
return (v, mode) => { | ||
if (!hasUnknown && objects.length > 0 && isObject(v)) { | ||
const item = objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return item.matcher(v, value, mode) as RawResult<Infer<T[number]>>; | ||
func(v: unknown, mode: FuncMode): RawResult<Infer<T[number]>> { | ||
if (!this.hasUnknown && this.objects.length > 0 && isObject(v)) { | ||
const item = this.objects[0]; | ||
let value = v[item.key]; | ||
if (value === undefined && !(item.key in v)) { | ||
value = Nothing; | ||
} | ||
return base(v, v, mode) as RawResult<Infer<T[number]>>; | ||
}; | ||
return item.matcher(v, value, mode) as RawResult<Infer<T[number]>>; | ||
} | ||
return this.base(v, v, mode) as RawResult<Infer<T[number]>>; | ||
} | ||
optional(): Optional<Infer<T[number]>> { | ||
return new Optional(this); | ||
} | ||
} | ||
class LiteralType<Out extends Literal = Literal> extends Type<Out> { | ||
readonly name = "literal"; | ||
constructor(readonly value: Out) { | ||
super(); | ||
} | ||
genFunc(): Func<Out> { | ||
const value = this.value; | ||
const issue: Issue = { code: "invalid_literal", expected: [value] }; | ||
return (v, _) => (v === value ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
class TransformType<Output> extends Type<Output> { | ||
readonly name = "transform"; | ||
private transformChain?: Func<unknown>[]; | ||
private transformRoot?: AbstractType; | ||
private readonly undef: RawResult<unknown> = { code: "ok", value: undefined }; | ||
constructor( | ||
@@ -1260,45 +1294,51 @@ protected readonly transformed: AbstractType, | ||
super(); | ||
this.transformChain = undefined; | ||
this.transformRoot = undefined; | ||
} | ||
genFunc(): Func<Output> { | ||
const chain: Func<unknown>[] = []; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next: AbstractType = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
} | ||
chain.reverse(); | ||
func(v: unknown, mode: FuncMode): RawResult<Output> { | ||
let chain = this.transformChain; | ||
if (!chain) { | ||
chain = []; | ||
const func = next.func; | ||
const undef = { code: "ok", value: undefined } as RawResult<unknown>; | ||
return (v, mode) => { | ||
let result = func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let next: AbstractType = this; | ||
while (next instanceof TransformType) { | ||
chain.push(next.transform); | ||
next = next.transformed; | ||
} | ||
chain.reverse(); | ||
this.transformChain = chain; | ||
this.transformRoot = next; | ||
} | ||
let current: unknown; | ||
if (result !== true) { | ||
current = result.value; | ||
} else if (v === Nothing) { | ||
current = undefined; | ||
result = undef; | ||
} else { | ||
current = v; | ||
} | ||
// eslint-disable-next-line | ||
let result = this.transformRoot!.func(v, mode); | ||
if (result !== true && result.code !== "ok") { | ||
return result; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
let current: unknown; | ||
if (result !== true) { | ||
current = result.value; | ||
} else if (v === Nothing) { | ||
current = undefined; | ||
result = this.undef; | ||
} else { | ||
current = v; | ||
} | ||
for (let i = 0; i < chain.length; i++) { | ||
const r = chain[i](current, mode); | ||
if (r !== true) { | ||
if (r.code !== "ok") { | ||
return r; | ||
} | ||
current = r.value; | ||
result = r; | ||
} | ||
return result as RawResult<Output>; | ||
}; | ||
} | ||
return result as RawResult<Output>; | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
@@ -1310,75 +1350,161 @@ this.transformed.toTerminals(into); | ||
readonly name = "lazy"; | ||
private recursing = false; | ||
private type?: Type<T>; | ||
constructor(private readonly definer: () => Type<T>) { | ||
super(); | ||
} | ||
private get type(): Type<T> { | ||
const type = this.definer(); | ||
Object.defineProperty(this, "type", { value: type }); | ||
return type; | ||
func(v: unknown, mode: FuncMode): RawResult<T> { | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
return this.type.func(v, mode); | ||
} | ||
genFunc(): Func<T> { | ||
let func: Func<T> | undefined = undefined; | ||
return (v, mode) => { | ||
if (!func) { | ||
func = this.type.func; | ||
toTerminals(into: TerminalType[]): void { | ||
if (this.recursing) { | ||
return; | ||
} | ||
try { | ||
this.recursing = true; | ||
if (!this.type) { | ||
this.type = this.definer(); | ||
} | ||
return func(v, mode); | ||
}; | ||
this.type.toTerminals(into); | ||
} finally { | ||
this.recursing = false; | ||
} | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
this.type.toTerminals(into); | ||
} | ||
class NeverType extends Type<never> { | ||
readonly name = "never"; | ||
private readonly issue: Issue = { code: "invalid_type", expected: [] }; | ||
func(_: unknown, __: FuncMode): RawResult<never> { | ||
return this.issue; | ||
} | ||
} | ||
const neverSingleton = new NeverType(); | ||
function never(): NeverType { | ||
return neverSingleton; | ||
} | ||
function singleton<Name extends string, Output>( | ||
name: Name, | ||
genFunc: () => Func<Output> | ||
): () => Type<Output> & { name: Name } { | ||
class Singleton extends Type<Output> { | ||
readonly name = name; | ||
genFunc(): Func<Output> { | ||
return genFunc(); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this as TerminalType); | ||
} | ||
class UnknownType extends Type<unknown> { | ||
readonly name = "unknown"; | ||
func(_: unknown, __: FuncMode): RawResult<unknown> { | ||
return true; | ||
} | ||
const instance = new Singleton(); | ||
return () => instance; | ||
} | ||
const unknownSingleton = new UnknownType(); | ||
function unknown(): UnknownType { | ||
return unknownSingleton; | ||
} | ||
const never = singleton("never", (): Func<never> => { | ||
const issue: Issue = { code: "invalid_type", expected: [] }; | ||
return (_v, _mode) => issue; | ||
}); | ||
const unknown = singleton("unknown", (): Func<unknown> => { | ||
return (_v, _mode) => true; | ||
}); | ||
const number = singleton("number", (): Func<number> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["number"] }; | ||
return (v, _mode) => (typeof v === "number" ? true : issue); | ||
}); | ||
const bigint = singleton("bigint", (): Func<bigint> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["bigint"] }; | ||
return (v, _mode) => (typeof v === "bigint" ? true : issue); | ||
}); | ||
const string = singleton("string", (): Func<string> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["string"] }; | ||
return (v, _mode) => (typeof v === "string" ? true : issue); | ||
}); | ||
const boolean = singleton("boolean", (): Func<boolean> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["boolean"] }; | ||
return (v, _mode) => (typeof v === "boolean" ? true : issue); | ||
}); | ||
const undefined_ = singleton("undefined", (): Func<undefined> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["undefined"] }; | ||
return (v, _mode) => (v === undefined ? true : issue); | ||
}); | ||
const null_ = singleton("null", (): Func<null> => { | ||
const issue: Issue = { code: "invalid_type", expected: ["null"] }; | ||
return (v, _mode) => (v === null ? true : issue); | ||
}); | ||
class UndefinedType extends Type<undefined> { | ||
readonly name = "undefined"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["undefined"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<undefined> { | ||
return v === undefined ? true : this.issue; | ||
} | ||
} | ||
const undefinedSingleton = new UndefinedType(); | ||
function undefined_(): UndefinedType { | ||
return undefinedSingleton; | ||
} | ||
class NullType extends Type<null> { | ||
readonly name = "null"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["null"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<null> { | ||
return v === null ? true : this.issue; | ||
} | ||
} | ||
const nullSingleton = new NullType(); | ||
function null_(): NullType { | ||
return nullSingleton; | ||
} | ||
class NumberType extends Type<number> { | ||
readonly name = "number"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["number"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<number> { | ||
return typeof v === "number" ? true : this.issue; | ||
} | ||
} | ||
const numberSingleton = new NumberType(); | ||
function number(): NumberType { | ||
return numberSingleton; | ||
} | ||
class BigIntType extends Type<bigint> { | ||
readonly name = "bigint"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["bigint"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<bigint> { | ||
return typeof v === "bigint" ? true : this.issue; | ||
} | ||
} | ||
const bigintSingleton = new BigIntType(); | ||
function bigint(): BigIntType { | ||
return bigintSingleton; | ||
} | ||
class StringType extends Type<string> { | ||
readonly name = "string"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["string"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<string> { | ||
return typeof v === "string" ? true : this.issue; | ||
} | ||
} | ||
const stringSingleton = new StringType(); | ||
function string(): StringType { | ||
return stringSingleton; | ||
} | ||
class BooleanType extends Type<boolean> { | ||
readonly name = "boolean"; | ||
private readonly issue: Issue = { | ||
code: "invalid_type", | ||
expected: ["boolean"], | ||
}; | ||
func(v: unknown, _: FuncMode): RawResult<boolean> { | ||
return typeof v === "boolean" ? true : this.issue; | ||
} | ||
} | ||
const booleanSingleton = new BooleanType(); | ||
function boolean(): BooleanType { | ||
return booleanSingleton; | ||
} | ||
class LiteralType<Out extends Literal = Literal> extends Type<Out> { | ||
readonly name = "literal"; | ||
private readonly issue: Issue; | ||
constructor(readonly value: Out) { | ||
super(); | ||
this.issue = { code: "invalid_literal", expected: [value] }; | ||
} | ||
func(v: unknown, _: FuncMode): RawResult<Out> { | ||
return v === this.value ? true : this.issue; | ||
} | ||
} | ||
function literal<T extends Literal>(value: T): Type<T> { | ||
return new LiteralType(value); | ||
} | ||
function object<T extends Record<string, Type | Optional>>( | ||
@@ -1389,8 +1515,11 @@ obj: T | ||
} | ||
function record<T extends Type>(valueType?: T): Type<Record<string, Infer<T>>> { | ||
return new ObjectType({} as Record<string, never>, valueType ?? unknown()); | ||
} | ||
function array<T extends Type>(item: T): ArrayType<[], T> { | ||
return new ArrayType([], item); | ||
} | ||
function tuple<T extends [] | [Type, ...Type[]]>( | ||
@@ -1401,5 +1530,7 @@ items: T | ||
} | ||
function union<T extends Type[]>(...options: T): Type<Infer<T[number]>> { | ||
return new UnionType(options); | ||
} | ||
function lazy<T>(definer: () => Type<T>): Type<T> { | ||
@@ -1406,0 +1537,0 @@ return new LazyType(definer); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
366938
6328