renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
437 lines • 13.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncResult = exports.Result = void 0;
const zod_1 = require("zod");
const logger_1 = require("../logger");
function isZodResult(input) {
if (typeof input !== 'object' ||
input === null ||
Object.keys(input).length !== 2 ||
!('success' in input) ||
typeof input.success !== 'boolean') {
return false;
}
if (input.success) {
return ('data' in input &&
typeof input.data !== 'undefined' &&
input.data !== null);
}
else {
return 'error' in input && input.error instanceof zod_1.ZodError;
}
}
function fromZodResult(input) {
return input.success ? Result.ok(input.data) : Result.err(input.error);
}
function fromNullable(input, errForNull, errForUndefined) {
if (input === null) {
return Result.err(errForNull);
}
if (input === undefined) {
return Result.err(errForUndefined);
}
return Result.ok(input);
}
/**
* Class for representing a result that can fail.
*
* The mental model:
* - `.wrap()` and `.wrapNullable()` are sinks
* - `.transform()` are pipes which can be chained
* - `.unwrap()` is the point of consumption
*/
class Result {
res;
constructor(res) {
this.res = res;
}
static ok(val) {
return new Result({ ok: true, val });
}
static err(err) {
return new Result({ ok: false, err });
}
static _uncaught(err) {
return new Result({ ok: false, err, _uncaught: true });
}
static wrap(input) {
if (isZodResult(input)) {
return fromZodResult(input);
}
if (input instanceof Promise) {
return AsyncResult.wrap(input);
}
try {
const result = input();
if (result instanceof Promise) {
return AsyncResult.wrap(result);
}
return Result.ok(result);
}
catch (error) {
return Result.err(error);
}
}
static wrapNullable(input, arg2, arg3) {
const errForNull = arg2;
const errForUndefined = arg3 ?? arg2;
if (input instanceof Promise) {
return AsyncResult.wrapNullable(input, errForNull, errForUndefined);
}
if (input instanceof Function) {
try {
const result = input();
return fromNullable(result, errForNull, errForUndefined);
}
catch (error) {
return Result.err(error);
}
}
return fromNullable(input, errForNull, errForUndefined);
}
/**
* Returns a discriminated union for type-safe consumption of the result.
* When error was uncaught during transformation, it's being re-thrown here.
*
* ```ts
*
* const { val, err } = Result.ok('foo').unwrap();
* expect(val).toBe('foo');
* expect(err).toBeUndefined();
*
* ```
*/
unwrap() {
if (this.res.ok) {
return this.res;
}
if (this.res._uncaught) {
// TODO: fix, should only allow `Error` type
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw this.res.err;
}
return this.res;
}
/**
* Returns a success value or a fallback value.
* When error was uncaught during transformation, it's being re-thrown here.
*
* ```ts
*
* const value = Result.err('bar').unwrapOr('foo');
* expect(val).toBe('foo');
*
* ```
*/
unwrapOr(fallback) {
if (this.res.ok) {
return this.res.val;
}
if (this.res._uncaught) {
// TODO: fix, should only allow `Error` type
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw this.res.err;
}
return fallback;
}
/**
* Returns the ok-value or throw the error.
*/
unwrapOrThrow() {
if (this.res.ok) {
return this.res.val;
}
// TODO: fix, should only allow `Error` type
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw this.res.err;
}
/**
* Returns the ok-value or `null`.
* When error was uncaught during transformation, it's being re-thrown here.
*/
unwrapOrNull() {
if (this.res.ok) {
return this.res.val;
}
if (this.res._uncaught) {
// TODO: fix, should only allow `Error` type
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw this.res.err;
}
return null;
}
transform(fn) {
if (!this.res.ok) {
return Result.err(this.res.err);
}
try {
const result = fn(this.res.val);
if (result instanceof Result) {
return result;
}
if (result instanceof AsyncResult) {
return result;
}
if (isZodResult(result)) {
return fromZodResult(result);
}
if (result instanceof Promise) {
return AsyncResult.wrap(result, (err) => {
logger_1.logger.warn({ err }, 'Result: unhandled async transform error');
return Result._uncaught(err);
});
}
return Result.ok(result);
}
catch (err) {
logger_1.logger.warn({ err }, 'Result: unhandled transform error');
return Result._uncaught(err);
}
}
catch(fn) {
if (this.res.ok) {
return this;
}
if (this.res._uncaught) {
return this;
}
try {
const result = fn(this.res.err);
if (result instanceof Promise) {
return AsyncResult.wrap(result, (err) => {
logger_1.logger.warn({ err }, 'Result: unexpected error in async catch handler');
return Result._uncaught(err);
});
}
return result;
}
catch (err) {
logger_1.logger.warn({ err }, 'Result: unexpected error in catch handler');
return Result._uncaught(err);
}
}
/**
* Given a `schema` and `input`, returns a `Result` with `val` being the parsed value.
* Additionally, `null` and `undefined` values are converted into Zod error.
*/
static parse(input, schema) {
const parseResult = schema
.transform((result, ctx) => {
if (result === undefined) {
ctx.addIssue({
code: zod_1.ZodIssueCode.custom,
message: `Result can't accept nullish values, but input was parsed by Zod schema to undefined`,
});
return zod_1.NEVER;
}
if (result === null) {
ctx.addIssue({
code: zod_1.ZodIssueCode.custom,
message: `Result can't accept nullish values, but input was parsed by Zod schema to null`,
});
return zod_1.NEVER;
}
return result;
})
.safeParse(input);
return fromZodResult(parseResult);
}
/**
* Given a `schema`, returns a `Result` with `val` being the parsed value.
* Additionally, `null` and `undefined` values are converted into Zod error.
*/
parse(schema) {
if (this.res.ok) {
return Result.parse(this.res.val, schema);
}
const err = this.res.err;
if (this.res._uncaught) {
return Result._uncaught(err);
}
return Result.err(err);
}
/**
* Call `fn` on the `val` if the result is ok.
*/
onValue(fn) {
if (this.res.ok) {
try {
fn(this.res.val);
}
catch (err) {
return Result._uncaught(err);
}
}
return this;
}
/**
* Call `fn` on the `err` if the result is err.
*/
onError(fn) {
if (!this.res.ok) {
try {
fn(this.res.err);
}
catch (err) {
return Result._uncaught(err);
}
}
return this;
}
}
exports.Result = Result;
/**
* This class is being used when `Result` methods encounter async code.
* It isn't meant to be used directly, but exported for usage in type annotations.
*
* All the methods resemble `Result` methods, but work asynchronously.
*/
class AsyncResult {
asyncResult;
constructor(asyncResult) {
this.asyncResult = asyncResult;
}
then(onfulfilled) {
return this.asyncResult.then(onfulfilled);
}
static ok(val) {
return new AsyncResult(Promise.resolve(Result.ok(val)));
}
static err(err) {
// eslint-disable-next-line promise/no-promise-in-callback
return new AsyncResult(Promise.resolve(Result.err(err)));
}
static wrap(promise, onErr) {
return new AsyncResult(promise
.then((value) => {
if (value instanceof Result) {
return value;
}
if (isZodResult(value)) {
return fromZodResult(value);
}
return Result.ok(value);
})
.catch((err) => {
if (onErr) {
return onErr(err);
}
return Result.err(err);
}));
}
static wrapNullable(promise, errForNull, errForUndefined) {
return new AsyncResult(promise
.then((value) => fromNullable(value, errForNull, errForUndefined))
.catch((err) => Result.err(err)));
}
/**
* Returns a discriminated union for type-safe consumption of the result.
*
* ```ts
*
* const { val, err } = await Result.wrap(readFile('foo.txt')).unwrap();
* expect(val).toBe('foo');
* expect(err).toBeUndefined();
*
* ```
*/
unwrap() {
return this.asyncResult.then((res) => res.unwrap());
}
/**
* Returns a success value or a fallback value.
*
* ```ts
*
* const val = await Result.wrap(readFile('foo.txt')).unwrapOr('bar');
* expect(val).toBe('bar');
* expect(err).toBeUndefined();
*
* ```
*/
unwrapOr(fallback) {
return this.asyncResult.then((res) => res.unwrapOr(fallback));
}
/**
* Returns the ok-value or throw the error.
*/
async unwrapOrThrow() {
const result = await this.asyncResult;
return result.unwrapOrThrow();
}
/**
* Returns the ok-value or `null`.
*/
unwrapOrNull() {
return this.asyncResult.then((res) => res.unwrapOrNull());
}
transform(fn) {
return new AsyncResult(this.asyncResult
.then((oldResult) => {
const { ok, val: value, err: error } = oldResult.unwrap();
if (!ok) {
return Result.err(error);
}
try {
const result = fn(value);
if (result instanceof Result) {
return result;
}
if (result instanceof AsyncResult) {
return result;
}
if (isZodResult(result)) {
return fromZodResult(result);
}
if (result instanceof Promise) {
return AsyncResult.wrap(result, (err) => {
logger_1.logger.warn({ err }, 'AsyncResult: unhandled async transform error');
return Result._uncaught(err);
});
}
return Result.ok(result);
}
catch (err) {
logger_1.logger.warn({ err }, 'AsyncResult: unhandled transform error');
return Result._uncaught(err);
}
})
.catch((err) => {
// Happens when `.unwrap()` of `oldResult` throws
return Result._uncaught(err);
}));
}
catch(fn) {
const caughtAsyncResult = this.asyncResult.then((result) =>
// eslint-disable-next-line promise/no-nesting
result.catch(fn));
return AsyncResult.wrap(caughtAsyncResult);
}
/**
* Given a `schema`, returns a `Result` with `val` being the parsed value.
* Additionally, `null` and `undefined` values are converted into Zod error.
*/
parse(schema) {
return new AsyncResult(this.asyncResult
.then((oldResult) => oldResult.parse(schema))
.catch(
/* istanbul ignore next: should never happen */
(err) => Result._uncaught(err)));
}
onValue(fn) {
return new AsyncResult(this.asyncResult
.then((result) => result.onValue(fn))
.catch(
/* istanbul ignore next: should never happen */
(err) => Result._uncaught(err)));
}
onError(fn) {
return new AsyncResult(this.asyncResult
.then((result) => result.onError(fn))
.catch(
/* istanbul ignore next: should never happen */
(err) => Result._uncaught(err)));
}
}
exports.AsyncResult = AsyncResult;
//# sourceMappingURL=result.js.map