Comparing version 0.1.1 to 0.1.2
@@ -8,2 +8,3 @@ "use strict"; | ||
exports.sig = | ||
exports.defAsync = | ||
exports.def = | ||
@@ -18,2 +19,8 @@ void 0; | ||
}); | ||
Object.defineProperty(exports, "defAsync", { | ||
enumerable: true, | ||
get: function () { | ||
return safunc_1.defAsync; | ||
}, | ||
}); | ||
Object.defineProperty(exports, "sig", { | ||
@@ -20,0 +27,0 @@ enumerable: true, |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.def = exports.sig = exports.optional = void 0; | ||
exports.defAsync = exports.def = exports.sig = exports.optional = void 0; | ||
const arktype_1 = require("arktype"); | ||
@@ -96,3 +96,3 @@ const ark_1 = require("./utils/ark"); | ||
$availableArgumentLengths, | ||
toString: () => { | ||
toString: ({ wrapReturnTypeWithPromise = false } = {}) => { | ||
let res = "("; | ||
@@ -108,3 +108,3 @@ res += $parameterSchemas | ||
if (!isUntyped($returnSchema)) | ||
res += `: ${(0, ark_1.stringifyDefinitionOf)($returnSchema)}`; | ||
res += `: ${wrapReturnTypeWithPromise ? "Promise<" + (0, ark_1.stringifyDefinitionOf)($returnSchema) + ">" : (0, ark_1.stringifyDefinitionOf)($returnSchema)}`; | ||
return res; | ||
@@ -115,2 +115,154 @@ }, | ||
exports.sig = sig; | ||
const _defBuilder = | ||
({ async }) => | ||
(...args) => { | ||
const sigs = args.slice(0, -1); | ||
const fn = args[args.length - 1]; | ||
let $matchedMorphedArguments = []; | ||
const matchArguments = (...args) => { | ||
const availableArgumentLengths = [ | ||
...new Set([...sigs.flatMap((sig) => sig.$availableArgumentLengths)]), | ||
].sort(); | ||
if (!availableArgumentLengths.includes(args.length)) { | ||
const message = `Expected ${(0, number_1.humanizeNaturalNumbers)(availableArgumentLengths)} arguments, but got ${args.length}`; | ||
throw new TypeError(message); | ||
} | ||
const sigAndMessages = []; | ||
for (let overloadIdx = 0; overloadIdx < sigs.length; overloadIdx++) { | ||
const sig = sigs[overloadIdx]; | ||
const { $availableArgumentLengths, $parameterSchemas } = sig; | ||
if (!$availableArgumentLengths.includes(args.length)) { | ||
sigAndMessages.push([sig, "ARG_LENGTH_NOT_MATCH"]); | ||
continue; | ||
} | ||
const morphedArgs = []; | ||
for (let i = 0; i < args.length; i++) { | ||
let validator = $parameterSchemas[i]; | ||
if (!validator) continue; | ||
if (isOptional(validator)) validator = validator[optionalSymbol]; | ||
const { data, problems } = validator(args[i]); | ||
if (!problems) { | ||
morphedArgs.push(data); | ||
continue; | ||
} | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += `the ${(0, number_1.ordinal)(i + 1)} argument of 'function`; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString({ wrapReturnTypeWithPromise: async }) + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${overloadIdx + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = (0, string_1.capitalize)(message); | ||
sigAndMessages.push([sig, message]); | ||
break; | ||
} | ||
if (!sigAndMessages[overloadIdx]) { | ||
$matchedMorphedArguments = morphedArgs; | ||
return sig; | ||
} | ||
} | ||
const errors = sigAndMessages | ||
.map(([sig, message], i) => ({ i, sig, message })) | ||
.filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH"); | ||
if (errors.length === 1) throw new TypeError(errors[0].message); | ||
let message = "No overload "; | ||
if (fn.name) message += `of function '${fn.name}' `; | ||
message += "matches this call.\n"; | ||
for (const { i, message: m, sig } of errors) { | ||
message += ` Overload ${i + 1} of ${sigs.length}, '${sig.toString({ wrapReturnTypeWithPromise: async })}', gave the following error.\n`; | ||
message += | ||
" " + | ||
m.replace( | ||
/argument of 'function.+?'( \(overload \d+ of \d+\))?/g, | ||
"argument", | ||
) + | ||
"\n"; | ||
} | ||
message = message.trimEnd(); | ||
throw new TypeError(message); | ||
}; | ||
const assertReturn = (sig, r) => { | ||
const { data, problems } = sig.$returnSchema(r); | ||
if (!problems) return data; | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += "the return value of 'function"; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString({ wrapReturnTypeWithPromise: async }) + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = (0, string_1.capitalize)(message); | ||
throw new TypeError(message); | ||
}; | ||
const f = (...args) => { | ||
const matchedSig = matchArguments(...args); | ||
if (!async) | ||
return assertReturn(matchedSig, fn(...$matchedMorphedArguments)); | ||
return new Promise((resolve, reject) => { | ||
void fn(...$matchedMorphedArguments).then((res) => { | ||
try { | ||
resolve(assertReturn(matchedSig, res)); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
}); | ||
}; | ||
// Keep the name of the function for better error messages | ||
Object.defineProperty(f, "name", { value: fn.name }); | ||
const res = f.bind(null); | ||
Object.defineProperty(res, "name", { value: fn.name }); | ||
Object.assign(res, { | ||
$sigs: sigs, | ||
$fn: fn, | ||
unwrap: () => f, | ||
matchArguments: (...args) => { | ||
try { | ||
return matchArguments(...args); | ||
} catch (e) { | ||
return null; | ||
} | ||
}, | ||
assertArguments: (...args) => { | ||
matchArguments(...args); | ||
}, | ||
allowArguments: (...args) => { | ||
try { | ||
matchArguments(...args); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
}); | ||
return res; | ||
}; | ||
/** | ||
@@ -167,141 +319,8 @@ * Create a type-safe function with runtime parameters and (optionally) return type validation. | ||
*/ | ||
exports.def = (...args) => { | ||
const sigs = args.slice(0, -1); | ||
const fn = args[args.length - 1]; | ||
let $matchedMorphedArguments = []; | ||
const matchArguments = (...args) => { | ||
const availableArgumentLengths = [ | ||
...new Set([...sigs.flatMap((sig) => sig.$availableArgumentLengths)]), | ||
].sort(); | ||
if (!availableArgumentLengths.includes(args.length)) { | ||
const message = `Expected ${(0, number_1.humanizeNaturalNumbers)(availableArgumentLengths)} arguments, but got ${args.length}`; | ||
throw new TypeError(message); | ||
} | ||
const sigAndMessages = []; | ||
for (let overloadIdx = 0; overloadIdx < sigs.length; overloadIdx++) { | ||
const sig = sigs[overloadIdx]; | ||
const { $availableArgumentLengths, $parameterSchemas } = sig; | ||
if (!$availableArgumentLengths.includes(args.length)) { | ||
sigAndMessages.push([sig, "ARG_LENGTH_NOT_MATCH"]); | ||
continue; | ||
} | ||
const morphedArgs = []; | ||
for (let i = 0; i < args.length; i++) { | ||
let validator = $parameterSchemas[i]; | ||
if (!validator) continue; | ||
if (isOptional(validator)) validator = validator[optionalSymbol]; | ||
const { data, problems } = validator(args[i]); | ||
if (!problems) { | ||
morphedArgs.push(data); | ||
continue; | ||
} | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += `the ${(0, number_1.ordinal)(i + 1)} argument of 'function`; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString() + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${overloadIdx + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = (0, string_1.capitalize)(message); | ||
sigAndMessages.push([sig, message]); | ||
break; | ||
} | ||
if (!sigAndMessages[overloadIdx]) { | ||
$matchedMorphedArguments = morphedArgs; | ||
return sig; | ||
} | ||
} | ||
const errors = sigAndMessages | ||
.map(([sig, message], i) => ({ i, sig, message })) | ||
.filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH"); | ||
if (errors.length === 1) throw new TypeError(errors[0].message); | ||
let message = "No overload "; | ||
if (fn.name) message += `of function '${fn.name}' `; | ||
message += "matches this call.\n"; | ||
for (const { i, message: m, sig } of errors) { | ||
message += ` Overload ${i + 1} of ${sigs.length}, '${sig.toString()}', gave the following error.\n`; | ||
message += | ||
" " + | ||
m.replace( | ||
/argument of 'function.+?'( \(overload \d+ of \d+\))?/g, | ||
"argument", | ||
) + | ||
"\n"; | ||
} | ||
message = message.trimEnd(); | ||
throw new TypeError(message); | ||
}; | ||
const assertReturn = (sig, r) => { | ||
const { data, problems } = sig.$returnSchema(r); | ||
if (!problems) return data; | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += "the return value of 'function"; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString() + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = (0, string_1.capitalize)(message); | ||
throw new TypeError(message); | ||
}; | ||
const f = (...args) => { | ||
const matchedSig = matchArguments(...args); | ||
return assertReturn(matchedSig, fn(...$matchedMorphedArguments)); | ||
}; | ||
// Keep the name of the function for better error messages | ||
Object.defineProperty(f, "name", { value: fn.name }); | ||
const res = f.bind(null); | ||
Object.defineProperty(res, "name", { value: fn.name }); | ||
Object.assign(res, { | ||
$sigs: sigs, | ||
$fn: fn, | ||
unwrap: () => f, | ||
matchArguments: (...args) => { | ||
try { | ||
return matchArguments(...args); | ||
} catch (e) { | ||
return null; | ||
} | ||
}, | ||
assertArguments: (...args) => { | ||
matchArguments(...args); | ||
}, | ||
allowArguments: (...args) => { | ||
try { | ||
matchArguments(...args); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
}); | ||
return res; | ||
}; | ||
exports.def = _defBuilder({ async: false }); | ||
/** | ||
* Create a type-safe asynchronous function with runtime parameters and (optionally) return type validation. | ||
* | ||
* Same as {@link def}, but the return type of the function must be a `Promise`. | ||
*/ | ||
exports.defAsync = _defBuilder({ async: true }); |
@@ -1,2 +0,2 @@ | ||
export { def, sig, optional } from "./safunc.js"; | ||
export { def, defAsync, sig, optional } from "./safunc.js"; | ||
export { record, unions, stringifyDefinitionOf } from "./utils/ark.js"; |
@@ -83,3 +83,3 @@ import { type } from "arktype"; | ||
$availableArgumentLengths, | ||
toString: () => { | ||
toString: ({ wrapReturnTypeWithPromise = false } = {}) => { | ||
let res = "("; | ||
@@ -95,3 +95,3 @@ res += $parameterSchemas | ||
if (!isUntyped($returnSchema)) | ||
res += `: ${stringifyDefinitionOf($returnSchema)}`; | ||
res += `: ${wrapReturnTypeWithPromise ? "Promise<" + stringifyDefinitionOf($returnSchema) + ">" : stringifyDefinitionOf($returnSchema)}`; | ||
return res; | ||
@@ -101,2 +101,154 @@ }, | ||
}; | ||
const _defBuilder = | ||
({ async }) => | ||
(...args) => { | ||
const sigs = args.slice(0, -1); | ||
const fn = args[args.length - 1]; | ||
let $matchedMorphedArguments = []; | ||
const matchArguments = (...args) => { | ||
const availableArgumentLengths = [ | ||
...new Set([...sigs.flatMap((sig) => sig.$availableArgumentLengths)]), | ||
].sort(); | ||
if (!availableArgumentLengths.includes(args.length)) { | ||
const message = `Expected ${humanizeNaturalNumbers(availableArgumentLengths)} arguments, but got ${args.length}`; | ||
throw new TypeError(message); | ||
} | ||
const sigAndMessages = []; | ||
for (let overloadIdx = 0; overloadIdx < sigs.length; overloadIdx++) { | ||
const sig = sigs[overloadIdx]; | ||
const { $availableArgumentLengths, $parameterSchemas } = sig; | ||
if (!$availableArgumentLengths.includes(args.length)) { | ||
sigAndMessages.push([sig, "ARG_LENGTH_NOT_MATCH"]); | ||
continue; | ||
} | ||
const morphedArgs = []; | ||
for (let i = 0; i < args.length; i++) { | ||
let validator = $parameterSchemas[i]; | ||
if (!validator) continue; | ||
if (isOptional(validator)) validator = validator[optionalSymbol]; | ||
const { data, problems } = validator(args[i]); | ||
if (!problems) { | ||
morphedArgs.push(data); | ||
continue; | ||
} | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += `the ${ordinal(i + 1)} argument of 'function`; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString({ wrapReturnTypeWithPromise: async }) + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${overloadIdx + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = capitalize(message); | ||
sigAndMessages.push([sig, message]); | ||
break; | ||
} | ||
if (!sigAndMessages[overloadIdx]) { | ||
$matchedMorphedArguments = morphedArgs; | ||
return sig; | ||
} | ||
} | ||
const errors = sigAndMessages | ||
.map(([sig, message], i) => ({ i, sig, message })) | ||
.filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH"); | ||
if (errors.length === 1) throw new TypeError(errors[0].message); | ||
let message = "No overload "; | ||
if (fn.name) message += `of function '${fn.name}' `; | ||
message += "matches this call.\n"; | ||
for (const { i, message: m, sig } of errors) { | ||
message += ` Overload ${i + 1} of ${sigs.length}, '${sig.toString({ wrapReturnTypeWithPromise: async })}', gave the following error.\n`; | ||
message += | ||
" " + | ||
m.replace( | ||
/argument of 'function.+?'( \(overload \d+ of \d+\))?/g, | ||
"argument", | ||
) + | ||
"\n"; | ||
} | ||
message = message.trimEnd(); | ||
throw new TypeError(message); | ||
}; | ||
const assertReturn = (sig, r) => { | ||
const { data, problems } = sig.$returnSchema(r); | ||
if (!problems) return data; | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += "the return value of 'function"; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString({ wrapReturnTypeWithPromise: async }) + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = capitalize(message); | ||
throw new TypeError(message); | ||
}; | ||
const f = (...args) => { | ||
const matchedSig = matchArguments(...args); | ||
if (!async) | ||
return assertReturn(matchedSig, fn(...$matchedMorphedArguments)); | ||
return new Promise((resolve, reject) => { | ||
void fn(...$matchedMorphedArguments).then((res) => { | ||
try { | ||
resolve(assertReturn(matchedSig, res)); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
}); | ||
}; | ||
// Keep the name of the function for better error messages | ||
Object.defineProperty(f, "name", { value: fn.name }); | ||
const res = f.bind(null); | ||
Object.defineProperty(res, "name", { value: fn.name }); | ||
Object.assign(res, { | ||
$sigs: sigs, | ||
$fn: fn, | ||
unwrap: () => f, | ||
matchArguments: (...args) => { | ||
try { | ||
return matchArguments(...args); | ||
} catch (e) { | ||
return null; | ||
} | ||
}, | ||
assertArguments: (...args) => { | ||
matchArguments(...args); | ||
}, | ||
allowArguments: (...args) => { | ||
try { | ||
matchArguments(...args); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
}); | ||
return res; | ||
}; | ||
/** | ||
@@ -153,141 +305,8 @@ * Create a type-safe function with runtime parameters and (optionally) return type validation. | ||
*/ | ||
export const def = (...args) => { | ||
const sigs = args.slice(0, -1); | ||
const fn = args[args.length - 1]; | ||
let $matchedMorphedArguments = []; | ||
const matchArguments = (...args) => { | ||
const availableArgumentLengths = [ | ||
...new Set([...sigs.flatMap((sig) => sig.$availableArgumentLengths)]), | ||
].sort(); | ||
if (!availableArgumentLengths.includes(args.length)) { | ||
const message = `Expected ${humanizeNaturalNumbers(availableArgumentLengths)} arguments, but got ${args.length}`; | ||
throw new TypeError(message); | ||
} | ||
const sigAndMessages = []; | ||
for (let overloadIdx = 0; overloadIdx < sigs.length; overloadIdx++) { | ||
const sig = sigs[overloadIdx]; | ||
const { $availableArgumentLengths, $parameterSchemas } = sig; | ||
if (!$availableArgumentLengths.includes(args.length)) { | ||
sigAndMessages.push([sig, "ARG_LENGTH_NOT_MATCH"]); | ||
continue; | ||
} | ||
const morphedArgs = []; | ||
for (let i = 0; i < args.length; i++) { | ||
let validator = $parameterSchemas[i]; | ||
if (!validator) continue; | ||
if (isOptional(validator)) validator = validator[optionalSymbol]; | ||
const { data, problems } = validator(args[i]); | ||
if (!problems) { | ||
morphedArgs.push(data); | ||
continue; | ||
} | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += `the ${ordinal(i + 1)} argument of 'function`; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString() + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${overloadIdx + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = capitalize(message); | ||
sigAndMessages.push([sig, message]); | ||
break; | ||
} | ||
if (!sigAndMessages[overloadIdx]) { | ||
$matchedMorphedArguments = morphedArgs; | ||
return sig; | ||
} | ||
} | ||
const errors = sigAndMessages | ||
.map(([sig, message], i) => ({ i, sig, message })) | ||
.filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH"); | ||
if (errors.length === 1) throw new TypeError(errors[0].message); | ||
let message = "No overload "; | ||
if (fn.name) message += `of function '${fn.name}' `; | ||
message += "matches this call.\n"; | ||
for (const { i, message: m, sig } of errors) { | ||
message += ` Overload ${i + 1} of ${sigs.length}, '${sig.toString()}', gave the following error.\n`; | ||
message += | ||
" " + | ||
m.replace( | ||
/argument of 'function.+?'( \(overload \d+ of \d+\))?/g, | ||
"argument", | ||
) + | ||
"\n"; | ||
} | ||
message = message.trimEnd(); | ||
throw new TypeError(message); | ||
}; | ||
const assertReturn = (sig, r) => { | ||
const { data, problems } = sig.$returnSchema(r); | ||
if (!problems) return data; | ||
const problem = problems[0]; | ||
const reason = problem.reason; | ||
let message = ""; | ||
// If the message is not just the reason | ||
if (problem.message.length !== reason.length) { | ||
let prefix = problem.message | ||
.toLowerCase() | ||
.slice(0, -reason.length) | ||
.trim(); | ||
// If it is likely a property name (contains no space and is not a number) | ||
if (!prefix.includes(" ") && isNaN(Number(prefix))) | ||
prefix = `Property '${prefix}'`; | ||
message += prefix + " of "; | ||
} | ||
message += "the return value of 'function"; | ||
// If function has a name | ||
if (fn.name) message += ` ${fn.name}`; | ||
message += sig.toString() + "' "; | ||
if (sigs.length > 1) | ||
message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `; | ||
message += reason; | ||
message = capitalize(message); | ||
throw new TypeError(message); | ||
}; | ||
const f = (...args) => { | ||
const matchedSig = matchArguments(...args); | ||
return assertReturn(matchedSig, fn(...$matchedMorphedArguments)); | ||
}; | ||
// Keep the name of the function for better error messages | ||
Object.defineProperty(f, "name", { value: fn.name }); | ||
const res = f.bind(null); | ||
Object.defineProperty(res, "name", { value: fn.name }); | ||
Object.assign(res, { | ||
$sigs: sigs, | ||
$fn: fn, | ||
unwrap: () => f, | ||
matchArguments: (...args) => { | ||
try { | ||
return matchArguments(...args); | ||
} catch (e) { | ||
return null; | ||
} | ||
}, | ||
assertArguments: (...args) => { | ||
matchArguments(...args); | ||
}, | ||
allowArguments: (...args) => { | ||
try { | ||
matchArguments(...args); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
}); | ||
return res; | ||
}; | ||
export const def = _defBuilder({ async: false }); | ||
/** | ||
* Create a type-safe asynchronous function with runtime parameters and (optionally) return type validation. | ||
* | ||
* Same as {@link def}, but the return type of the function must be a `Promise`. | ||
*/ | ||
export const defAsync = _defBuilder({ async: true }); |
{ | ||
"name": "safunc", | ||
"version": "0.1.1", | ||
"description": "Create runtime-validated functions with ease, supporting optional parameters and overloaded signatures with smart type inference in TypeScript", | ||
"version": "0.1.2", | ||
"description": "Create runtime-validated functions for both synchronous and asynchronous ones with ease, supporting optional parameters and overloaded signatures with smart type inference in TypeScript", | ||
"keywords": [ | ||
@@ -6,0 +6,0 @@ "typescript", |
<h1 align="center">Safunc</h1> | ||
<p align="center"> | ||
Create <strong><i>runtime-validated</i> functions</strong> with ease, featuring <strong>smart type inference</strong> in TypeScript. | ||
Create <strong><i>runtime-validated</i> functions</strong> for both <strong>synchronous</strong> and <strong>asynchronous</strong> ones with ease, featuring <strong>smart type inference</strong> in TypeScript. | ||
</p> | ||
@@ -25,3 +25,3 @@ | ||
Safunc is a small utility library that allows you to create functions with **runtime validation** of arguments and (optionally) return values, supporting **optional parameters** and **overloaded signatures** with **smart type inference** in TypeScript. It is powered by [Arktype](https://github.com/arktypeio/arktype), an amazing runtime type-checking library using almost 1:1 syntax with TypeScript. | ||
Safunc is a small utility library that allows you to create both **synchronous** and **asynchronous** functions with **runtime validation** of arguments and (optionally) return values, supporting **optional parameters** and **overloaded signatures** with **smart type inference** in TypeScript. It is powered by [Arktype](https://github.com/arktypeio/arktype), an amazing runtime type-checking library using almost 1:1 syntax with TypeScript. | ||
@@ -96,2 +96,54 @@  | ||
### Asynchronous Functions | ||
When working with asynchronous functions, such as those commonly found in REST API calls, it is likely you want to validate the arguments and return types if the API is unreliable or the data is critical. Safunc facilitates this with the `defAsync` function, which is used in place of `def`: | ||
```typescript | ||
import { arrayOf, type } from "arktype"; | ||
import { defAsync, sig } from "safunc"; | ||
type Todo = typeof todo.infer; | ||
const todo = type({ | ||
userId: "integer>0", | ||
id: "integer>0", | ||
title: "string", | ||
completed: "boolean", | ||
}); | ||
const getTodos = defAsync(sig("=>", arrayOf(todo)), async () => { | ||
// ^?: Safunc<() => Promise<Todo[]>> | ||
const res = await fetch("https://jsonplaceholder.typicode.com/todos"); | ||
return res.json() as Promise<Todo[]>; | ||
}); | ||
await getTodos(); // => [{ userId: 1, id: 1, title: "delectus aut autem", completed: false }, ...] | ||
type TodoWrong = typeof todoWrong.infer; | ||
const todoWrong = type({ | ||
userId: "integer>0", | ||
id: "string>0", // <- This will throw a TypeError | ||
title: "string", | ||
completed: "boolean", | ||
}); | ||
const getTodosWrong = defAsync(sig("=>", arrayOf(todoWrong)), async () => { | ||
// ^?: Safunc<() => Promise<TodoWrong[]>> | ||
const res = await fetch("https://jsonplaceholder.typicode.com/todos"); | ||
return res.json() as Promise<TodoWrong[]>; | ||
}); | ||
await getTodosWrong(); // !TypeError: Property '0/id' of the return value of 'function(): Promise<Array<{ userId: integer>0; id: string>0; title: string; completed: boolean }>>' must be a string (was number) | ||
const getTodo = defAsync( | ||
// ^?: Safunc<(id: number) => Promise<Todo>> | ||
sig("integer>0", "=>", todo), | ||
async (id) => | ||
await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then( | ||
(res) => res.json() as Promise<Todo>, | ||
), | ||
); | ||
getTodo(0.5); // !TypeError: The 1st argument of 'function(integer>0): Promise<{ userId: integer>0; id: integer>0; title: string; completed: boolean }>' must be an integer (was 0.5) | ||
await getTodo(1); // => { userId: 1, id: 1, title: "delectus aut autem", completed: false } | ||
``` | ||
`defAsync` supports all features of `def`, including optional parameters and overloaded signatures, which will be discussed later. The only difference is that `defAsync` requires functions to return a `Promise`, and validation of the return value is handled asynchronously (while the arguments are still validated synchronously). | ||
### Optional Parameters | ||
@@ -98,0 +150,0 @@ |
@@ -1,3 +0,3 @@ | ||
export { def, sig, optional } from "./safunc"; | ||
export { def, defAsync, sig, optional } from "./safunc"; | ||
export { record, unions, stringifyDefinitionOf } from "./utils/ark"; | ||
export type { Safunc, Sig, SigInOut, untyped } from "./safunc"; |
@@ -77,3 +77,3 @@ import type { Eq, Fn } from "./tools/common"; | ||
$availableArgumentLengths: readonly number[]; | ||
toString: () => string; | ||
toString: (opts?: { wrapReturnTypeWithPromise?: boolean }) => string; | ||
} | ||
@@ -542,2 +542,251 @@ type AsOutAll<TS extends unknown[]> = | ||
}; | ||
type Asyncify<F extends Fn> = | ||
F extends infer F extends Fn ? | ||
(...args: Parameters<F>) => Promise<ReturnType<F>> | ||
: never; | ||
/** | ||
* Create a type-safe asynchronous function with runtime parameters and (optionally) return type validation. | ||
* | ||
* Same as {@link def}, but the return type of the function must be a `Promise`. | ||
*/ | ||
export declare const defAsync: { | ||
< | ||
FIn extends Fn, | ||
FOut extends Fn, | ||
F extends Asyncify<RefineUntyped<FIn, any>>, | ||
>( | ||
sig: SigInOut<FIn, FOut>, | ||
fn: F, | ||
): Safunc< | ||
( | ||
...args: LabeledBy<Parameters<F>, Parameters<FOut>> | ||
) => Promise<IfUntyped<ReturnType<FOut>, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
F extends ( | ||
...args: Parameters<FIn1> | Parameters<FIn2> | ||
) => Promise<FInReturn<[FIn1, FIn2]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
F extends ( | ||
...args: Parameters<FIn1> | Parameters<FIn2> | Parameters<FIn3> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
FIn4 extends Fn, | ||
FOut4 extends Fn, | ||
F extends ( | ||
...args: | ||
| Parameters<FIn1> | ||
| Parameters<FIn2> | ||
| Parameters<FIn3> | ||
| Parameters<FIn4> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3, FIn4]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
sig4: SigInOut<FIn4, FOut4>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut4, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
FIn4 extends Fn, | ||
FOut4 extends Fn, | ||
FIn5 extends Fn, | ||
FOut5 extends Fn, | ||
F extends ( | ||
...args: | ||
| Parameters<FIn1> | ||
| Parameters<FIn2> | ||
| Parameters<FIn3> | ||
| Parameters<FIn4> | ||
| Parameters<FIn5> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3, FIn4, FIn5]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
sig4: SigInOut<FIn4, FOut4>, | ||
sig5: SigInOut<FIn5, FOut5>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut4, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut5, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
FIn4 extends Fn, | ||
FOut4 extends Fn, | ||
FIn5 extends Fn, | ||
FOut5 extends Fn, | ||
FIn6 extends Fn, | ||
FOut6 extends Fn, | ||
F extends ( | ||
...args: | ||
| Parameters<FIn1> | ||
| Parameters<FIn2> | ||
| Parameters<FIn3> | ||
| Parameters<FIn4> | ||
| Parameters<FIn5> | ||
| Parameters<FIn6> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3, FIn4, FIn5, FIn6]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
sig4: SigInOut<FIn4, FOut4>, | ||
sig5: SigInOut<FIn5, FOut5>, | ||
sig6: SigInOut<FIn6, FOut6>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut4, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut5, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut6, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
FIn4 extends Fn, | ||
FOut4 extends Fn, | ||
FIn5 extends Fn, | ||
FOut5 extends Fn, | ||
FIn6 extends Fn, | ||
FOut6 extends Fn, | ||
FIn7 extends Fn, | ||
FOut7 extends Fn, | ||
F extends ( | ||
...args: | ||
| Parameters<FIn1> | ||
| Parameters<FIn2> | ||
| Parameters<FIn3> | ||
| Parameters<FIn4> | ||
| Parameters<FIn5> | ||
| Parameters<FIn6> | ||
| Parameters<FIn7> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3, FIn4, FIn5, FIn6, FIn7]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
sig4: SigInOut<FIn4, FOut4>, | ||
sig5: SigInOut<FIn5, FOut5>, | ||
sig6: SigInOut<FIn6, FOut6>, | ||
sig7: SigInOut<FIn7, FOut7>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut4, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut5, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut6, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut7, ReturnType<F>>> | ||
>; | ||
< | ||
FIn1 extends Fn, | ||
FOut1 extends Fn, | ||
FIn2 extends Fn, | ||
FOut2 extends Fn, | ||
FIn3 extends Fn, | ||
FOut3 extends Fn, | ||
FIn4 extends Fn, | ||
FOut4 extends Fn, | ||
FIn5 extends Fn, | ||
FOut5 extends Fn, | ||
FIn6 extends Fn, | ||
FOut6 extends Fn, | ||
FIn7 extends Fn, | ||
FOut7 extends Fn, | ||
FIn8 extends Fn, | ||
FOut8 extends Fn, | ||
F extends ( | ||
...args: | ||
| Parameters<FIn1> | ||
| Parameters<FIn2> | ||
| Parameters<FIn3> | ||
| Parameters<FIn4> | ||
| Parameters<FIn5> | ||
| Parameters<FIn6> | ||
| Parameters<FIn7> | ||
| Parameters<FIn8> | ||
) => Promise<FInReturn<[FIn1, FIn2, FIn3, FIn4, FIn5, FIn6, FIn7, FIn8]>>, | ||
>( | ||
sig1: SigInOut<FIn1, FOut1>, | ||
sig2: SigInOut<FIn2, FOut2>, | ||
sig3: SigInOut<FIn3, FOut3>, | ||
sig4: SigInOut<FIn4, FOut4>, | ||
sig5: SigInOut<FIn5, FOut5>, | ||
sig6: SigInOut<FIn6, FOut6>, | ||
sig7: SigInOut<FIn7, FOut7>, | ||
sig8: SigInOut<FIn8, FOut8>, | ||
fn: F, | ||
): Safunc< | ||
Asyncify<RefineUntyped<FOut1, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut2, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut3, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut4, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut5, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut6, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut7, ReturnType<F>>> & | ||
Asyncify<RefineUntyped<FOut8, ReturnType<F>>> | ||
>; | ||
}; | ||
export {}; |
2168298
3477
468