diff --git a/src/error/ChainedError.ts b/src/error/ChainedError.ts new file mode 100644 index 0000000..efe9b99 --- /dev/null +++ b/src/error/ChainedError.ts @@ -0,0 +1,73 @@ +/** + * Uniform error handling for Liza: error chaining + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { ___Writable } from 'naughty'; + + +/** + * An Error augmented to include information about an underlying cause + * + * To create new chains, use the `chain` function. + * + * Chaining should be used when an error is caught and transformed into + * another, more specific error. By maintaining a reference to an existing + * error, context is not lost, which can be helpful for debugging and + * logging. + * + * Chains may be nested to an arbitrary depth, but because of the nature of + * JavaScript's errors, recursive chain type checks must be done at runtime. + */ +export interface ChainedError extends Error +{ + readonly chain: T, +} + + +/** + * Type predicate for `ChainedError` + * + * This predicate can be used at runtime to determine whether an error is + * chained. + * + * @param e error object + * + * @return whether `e` is of type ChainedError + */ +export const isChained = ( e: Error ): e is ChainedError => + ( e ).chain !== undefined; + + +/** + * Chains two `Error`s + * + * This is intended to be used as if it were a constructor, where the first + * argument is a new `Error` instance. + * + * @param enew new error + * @param eprev error to chain + * + * @return `enew` with `eprev` chained + */ +export function chain( enew: Error, eprev: Error ): ChainedError +{ + ( <___Writable>enew ).chain = eprev; + return enew; +} diff --git a/src/error/ContextError.ts b/src/error/ContextError.ts new file mode 100644 index 0000000..1286a8b --- /dev/null +++ b/src/error/ContextError.ts @@ -0,0 +1,93 @@ +/** + * Uniform error handling for Liza: error context + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * This context system is intended to play nicely with how Error objects are + * typically used in JavaScript. As such, rather than creating new error + * prototypes / subclasses, these rely on structural typing to augment + * existing `Error` objects. + */ + +import { ___Writable } from 'naughty'; + + +/** + * Error with additional context regarding its cause + * + * Errors may be augmented with key/value data (see `ErrorContext`) + * containing data that will be helpful for debugging the cause of the + * error. The context should be expected to appear in structured logs, so + * it shouldn't include sensitive data without some mitigation layer. + * + * A context may be optionally typed, but note that such context will + * generally be lost any time promises are used, so type predicates will + * need to be used to restore the type with information at runtime. + * + * Since the context is intended primarily for debugging, it shouldn't be + * relied on to drive control flow unless absolutely necessary, in which + * case an explicit context should be used. + */ +export interface ContextError + extends Error +{ + readonly context: T, +} + + +/** + * Key/value context for an error + * + * Rather than accepting data of an arbitrary type, we force key/value for + * reasons of extensibility and consistency: if more information is needed + * in the future, the type will remain unchanged. The values, however, may + * include any arbitrary data. + */ +export type ErrorContext = { readonly [P: string]: any }; + + +/** + * Type predicate for `ContextError` + * + * Note that this is a predicate for a generic `ContextError`, type, which + * is equivalent to `ContextError`. Other contexts must + * define their own predicates. + * + * @param e error object + * + * @return whether `e` is of type `ContextError` + */ +export const hasContext = ( e: Error ): e is ContextError => + ( e ).context !== undefined; + + +/** + * Adds context to an error + * + * This is intended to be used as if it were a constructor, where the first + * argument is a new `Error` instance. + * + * @param enew error object to add context to + * @param context key/value context information + */ +export function context( enew: Error, context: T ): + ContextError +{ + ( <___Writable>>enew ).context = context; + return >enew; +} diff --git a/src/types/naugty.d.ts b/src/types/naugty.d.ts new file mode 100644 index 0000000..cdca847 --- /dev/null +++ b/src/types/naugty.d.ts @@ -0,0 +1,48 @@ +/** + * Things that should only be used when absolutely necessary + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * DEFINITIONS IN THIS PACKAGE DO NAUGHTY THINGS THAT CIRCUMVENT TYPE + * SAFETY; THEY SHOULD BE USED ONLY WHEN NECESSARY, AND ONLY WHEN YOU KNOW + * WHAT YOU'RE DOING, SINCE THEY MAY INTRODUCE BUGS! + * + * The prefix `___` is added to each of the names here so that code can be + * easily searched for uses of naughty things. + * + * These types are also exported, unlike some other `.d.ts` files which are + * universally available during complication---this forces the importing of + * this file, named `naughty.d.ts`, which should raise some eyebrows and + * make people less likely to copy existing code that uses it. + */ + +declare module 'naughty' +{ + /** + * Make type `T` writable while otherwise maintaining type safety + * + * _Only use this generic if you are the owner of the object being + * manipulated!__ + * + * This should be used when we want types to be readonly, but we need to + * be able to modify an existing object to initialize the + * properties. This should only be used in situations where it's not + * feasible to add those properties when the object is first created. + */ + export type ___Writable = { -readonly [K in keyof T]: T[K] }; +} diff --git a/test/error/ChainedErrorTest.ts b/test/error/ChainedErrorTest.ts new file mode 100644 index 0000000..d6d98ed --- /dev/null +++ b/test/error/ChainedErrorTest.ts @@ -0,0 +1,67 @@ +/** + * Tests error chaining + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as sut from "../../src/error/ChainedError"; +import { expect } from 'chai'; + + +describe( 'ChainedError', () => +{ + it( "can be created with generic error", () => + { + const eprev = new Error( "previous error" ); + + expect( sut.chain( new Error( "new error" ), eprev ).chain ) + .to.equal( eprev ); + } ); + + + it( "can be chained to arbitrary depth", () => + { + const e1 = new Error( "lower" ); + const e2 = sut.chain( new Error( "mid" ), e1 ); + const e3 = sut.chain( new Error( "outer" ), e2 ); + + expect( sut.isChained( e3 ) ).to.be.true; + expect( sut.isChained( e2 ) ).to.be.true; + expect( sut.isChained( e1 ) ).to.be.false; + } ); + + + it( "provides type predicate for TypeScript", () => + { + const inner = new Error( "inner" ); + + // force to Error to discard ChainedError type + const outer: Error = sut.chain( new Error( "outer" ), inner ); + + if ( sut.isChained( outer ) ) + { + // if isChained was properly defined, then outer should now + // have type ChainedError, and so this should compile + expect( outer.chain ).to.equal( inner ); + } + else + { + expect.fail(); + } + } ); +} ); diff --git a/test/error/ContextErrorTest.ts b/test/error/ContextErrorTest.ts new file mode 100644 index 0000000..c02c3f2 --- /dev/null +++ b/test/error/ContextErrorTest.ts @@ -0,0 +1,68 @@ +/** + * Tests error context + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as sut from "../../src/error/ContextError"; +import { expect } from 'chai'; + + +describe( 'ContextError', () => +{ + it( "can be created with generic error", () => + { + const context = { foo: "context" }; + + expect( sut.context( new Error( "test error" ), context ).context ) + .to.equal( context ); + } ); + + + it( "provides type predicate for TypeScript", () => + { + const context = { bar: "baz context" }; + + // force to Error to discard ContextError type + const e: Error = sut.context( new Error( "test error" ), context ); + + if ( sut.hasContext( e ) ) + { + // if isChained was properly defined, then outer should now + // have type ChainedError, and so this should compile + expect( e.context ).to.equal( context ); + } + else + { + expect.fail(); + } + } ); + + + it( "can create typed contexts", () => + { + type FooErrorContext = { foo: string }; + + // this is the actual test + const e: sut.ContextError = + sut.context( new Error( "test error" ), { foo: "context" } ); + + // contravariance check (would fail to compile) + expect( sut.hasContext( e ) ).to.be.true; + } ); +} );