@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
253 lines (249 loc) • 8.2 kB
JavaScript
;
var zod = require('zod');
/**
* Decorator functions that can be used to validate the input and output of class
* methods and guarantee type safety at runtime, include `param`, `returns` and
* `throws`, based on [Zod](https://zod.dev).
* @module
*/
const _source = Symbol.for("source");
const _params = Symbol.for("params");
const _returns = Symbol.for("returns");
const _throws = Symbol.for("throws");
function refinedError(err, ctorOpt) {
var _a;
if (err instanceof zod.z.ZodError) {
let issue = err.issues[0];
if (issue.code === "invalid_union") {
// report the first error encountered
issue = issue.unionErrors[0].issues[0];
}
const path = issue.path.join(".");
const _message = issue.message[0].toLowerCase() + issue.message.slice(1);
const message = `validation failed at ${path}: `
+ _message.replace(/^input not instance of/, "not an instance of");
// @ts-ignore cause is not available in new ECMA standard
err = new TypeError(message, { cause: err });
(_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, err, ctorOpt);
}
return err;
}
function decorate(target, prop = void 0, desc = null) {
const fn = desc ? desc.value : target;
if (_source in fn) {
return fn;
}
const originFn = fn;
prop
|| (typeof target === "function" ? target.name : "")
|| "anonymous";
const newFn = (function (...args) {
const paramsDef = newFn[_params];
const returnDef = newFn[_returns];
const throwDef = newFn[_throws];
if (paramsDef === null || paramsDef === void 0 ? void 0 : paramsDef.length) {
const keyedArgs = {};
const params = paramsDef.map((item, index) => {
const name = item.name || `arg${index}`;
keyedArgs[name] = args[index];
return { ...item, name };
});
for (let i = Object.keys(keyedArgs).length; i < args.length; i++) {
keyedArgs[`arg${i}`] = args[i];
}
try {
for (const { name, type } of params) {
keyedArgs[name] = type.parse(keyedArgs[name], {
path: ["parameter " + name],
});
}
}
catch (err) {
throw refinedError(err, newFn);
}
args = Object.values(keyedArgs);
}
const handleError = (err, onrejected = null) => {
if (err instanceof zod.z.ZodError) {
throw refinedError(err, onrejected !== null && onrejected !== void 0 ? onrejected : newFn);
}
else if (throwDef) {
try {
err = throwDef.type.parse(err, { path: [throwDef.name] });
}
catch (_err) {
err = refinedError(_err, onrejected !== null && onrejected !== void 0 ? onrejected : newFn);
}
throw err;
}
else {
throw err;
}
};
try {
let returns = originFn.apply(this, args);
if (returnDef) {
returns = returnDef.type.parse(returns, { path: [returnDef.name] });
}
if (returns && typeof returns === "object" && typeof returns.then === "function") {
return Promise.resolve(returns).catch(function catcher(err) {
handleError(err, catcher);
});
}
else {
return returns;
}
}
catch (err) {
handleError(err);
}
});
newFn[_source] = originFn;
Object.defineProperty(newFn, "name", Object.getOwnPropertyDescriptor(originFn, "name"));
Object.defineProperty(newFn, "length", Object.getOwnPropertyDescriptor(originFn, "length"));
Object.defineProperty(newFn, "toString", {
configurable: true,
enumerable: false,
writable: true,
value: originFn.toString.bind(originFn),
});
if (desc) {
return (desc.value = newFn);
}
else {
return newFn;
}
}
function param(arg1, arg2 = undefined) {
let name;
let type;
if (typeof arg1 === "string") {
name = arg1;
type = arg2;
}
else {
name = undefined;
type = arg1;
}
return (...args) => {
var _a, _b;
if (typeof args[1] === "object") { // new ES decorator since TypeScript 5.0
const [target, context] = args;
const fn = decorate(target, context.name);
const params = ((_a = fn[_params]) !== null && _a !== void 0 ? _a : (fn[_params] = []));
params.unshift({ type, name });
return fn;
}
else {
const [target, prop, desc] = args;
const fn = decorate(target, prop, desc);
const params = ((_b = fn[_params]) !== null && _b !== void 0 ? _b : (fn[_params] = []));
params.unshift({ type, name });
return desc;
}
};
}
/**
* A decorator that restrains the return value of the method at runtime, based
* on [Zod](https://zod.dev).
*
* @example
* ```ts
* // regular function
* import { returns } from "@ayonli/jsext/class/decorators";
* import { z } from "zod";
*
* class Calculator {
* \@returns(z.number())
* times(a: number, b: number) {
* return String(a * b);
* }
* }
*
* const calc = new Calculator();
* console.log(calc.times(2, 3));
* // throws:
* // TypeError: validation failed at return value: expected number, received string
* ```
*
* @example
* ```ts
* // async function
* import { returns } from "@ayonli/jsext/class/decorators";
* import { z } from "zod";
*
* class Calculator {
* \@returns(z.promise(z.number()))
* async times(a: number, b: number) {
* await new Promise(resolve => setTimeout(resolve, 100));
* return String(a * b);
* }
* }
*
* const calc = new Calculator();
* console.log(await calc.times(2, 3));
* // throws:
* // TypeError: validation failed at return value: expected number, received string
* ```
*/
function returns(type) {
return (...args) => {
if (typeof args[1] === "object") { // new ES decorator since TypeScript 5.0
const [target, context] = args;
const fn = decorate(target, context.name);
fn[_returns] = { type, name: "return value" };
return fn;
}
else {
const [target, prop, desc] = args;
const fn = decorate(target, prop, desc);
fn[_returns] = { type, name: "return value" };
return desc;
}
};
}
/**
* A decorator that restrains the thrown value of the method at runtime, based
* on [Zod](https://zod.dev).
*
* @example
* ```ts
* import { throws } from "@ayonli/jsext/class/decorators";
* import { z } from "zod";
*
* class Calculator {
* \@throws(z.instanceof(RangeError))
* div(a: number, b: number) {
* if (b === 0) {
* throw new TypeError("division by zero");
* }
* return a / b;
* }
* }
*
* const calc = new Calculator();
* console.log(calc.div(2, 0));
* // throws:
* // TypeError: validation failed at thrown value: not an instance of RangeError
* ```
*/
function throws(type) {
return (...args) => {
if (typeof args[1] === "object") { // new ES decorator since TypeScript 5.0
const [target, context] = args;
const fn = decorate(target, context.name);
fn[_throws] = { type, name: "thrown value" };
return fn;
}
else {
const [target, prop, desc] = args;
const fn = decorate(target, prop, desc);
fn[_throws] = { type, name: "thrown value" };
return desc !== null && desc !== void 0 ? desc : fn;
}
};
}
exports.param = param;
exports.returns = returns;
exports.throws = throws;
//# sourceMappingURL=decorators.js.map