UNPKG

@ibgib/helper-gib

Version:

common helper/utils/etc used in ibgib libs. Node v19+ needed for heavily-used isomorphic webcrypto hashing consumed in both node and browsers.

428 lines (398 loc) 15.5 kB
import { labelize } from "./respec-gib-helper.mjs"; export interface InfoBase { /** * Log context. */ lc?: string; /** * If true, performs lots of trace verbose logging respeculation stuff. */ logalot?: boolean; } export type MaybeAsyncFn<TReturn = void> = (() => TReturn) | (() => Promise<TReturn>); export interface RespecFunctionInfo extends InfoBase { fn: MaybeAsyncFn; } export interface IfWeBlock extends InfoBase { } export interface RespecInfo extends InfoBase { /** * file path of spec. use meta.import.url */ title: string; kind: 'respecfully' | 'ifwe'; bodyFn: MaybeAsyncFn; parent?: RespecInfo; subBlocks: RespecInfo[]; /** * function(s) that execute before any respeculations. */ fnFirstOfAlls: MaybeAsyncFn[]; fnFirstOfAllsComplete?: boolean; /** * function(s) that execute before each respeculation. */ fnFirstOfEachs: MaybeAsyncFn[]; /** * function(s) that execute after any respeculations. */ fnLastOfAlls: MaybeAsyncFn[]; fnLastOfAllsComplete?: boolean; /** * function(s) that execute after each respeculation. */ fnLastOfEachs: MaybeAsyncFn[]; /** * If the respecful block contains a reckoning via `iReckon`, then this will * be contain the resulting object of that reckoning. */ reckonings: Reckoning[]; /** * if true, block as completed execution */ complete?: boolean; error?: any; /** * todo: stopOnFailure */ // stopOnFailure: false, /** * If true, then this block is being focused on in testing. This is a way * to isolate testing (like if you're working on testing one particular * function). */ extraRespecful?: boolean; } export interface RespecOptions { // stopOnFailure?: boolean; overrideLc?: string; logalot?: boolean; /** * If true, then we're just checking the respec lib itself. This will not * include the block/reckoning in the final report. */ justMetaTesting?: boolean; /** * If true, then this block is being focused on in testing. This is a way * to isolate testing (like if you're working on testing one particular * function). */ extraRespecful?: boolean; } /** * todo: fill this out with whitelisted function names. */ export type ReckoningFunctionName = 'willEqual' | string; export interface ReckoningOptions { overrideLc?: string; addedMsg?: string; /** * If true, then we're just checking the respec lib itself. This will not * include the block/reckoning in the final report. */ justMetaTesting?: boolean; } export interface ReckoningInfo { /** * fn name to execute */ fnName?: ReckoningFunctionName | undefined; /** * need to rename. This is what the reckoning is testing against this.value * for some tests. */ x?: any | undefined; /** * Optional addedMsg */ addedMsg: string | undefined; } export class RespecGib { /** * All respec paths that were found with the given regexp and root * directory. */ allRespecPaths: string[] = []; /** * the paths of respec files after taking into account filtering. * This includes atow "extra respec". */ filteredRespecPaths: string[] | undefined; /** * Paths to respec files that were actually done. This is either going to be * `allRespecPaths` or `filteredRespecPaths`. */ respecPaths: string[] = []; /** * If true, then we are looking for - and have found - files/blocks with * extra respec. * * Note that atow (03/2024), this affects ONLY reporting of test results. In * the future, we may add a separate flag. */ extraRespecOnly: boolean = false; respecs: { [title: string]: RespecInfo[] } = {}; errorMsgs: string[] = []; ifWeBlocksSkipped: number = 0; } export class Reckoning { private extraLabel: string | undefined = undefined; failMsg: string | undefined = undefined; justMetaTesting: boolean | undefined = undefined; #not: boolean = false; get not(): Reckoning { this.#not = !this.#not; return this; } completed: Promise<boolean> | undefined = undefined; constructor( private value: any, private logalot: boolean = false, ) { } /** * gives extra context for the test message. * * @param extraLabel for additional context info for the reckoning * @returns this reckoning for fluent method-chaining */ asTo(extraLabel: string): Reckoning { this.extraLabel = extraLabel; return this; } /** * wrapper/synonym for {@link asTo}. * * @see {@link asTo} */ wrt(extraLabel: string): Reckoning { return this.asTo(extraLabel); } /** * checks for strict equals ===. * * @param x value to test against this.value * @param opts * @returns this reckoning for fluent method-chaining */ willEqual(x: any, opts?: ReckoningOptions): Reckoning { const lc = `[${this.willEqual.name}]`; const { logalot } = this; const { addedMsg, justMetaTesting } = opts ?? {}; try { if (logalot) { console.log(`${lc} starting... (I: a07da12997b56d62347be2a71d984a23)`); } this.justMetaTesting = justMetaTesting; if (typeof x !== typeof this.value && !this.#not) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) type (${typeof this.value}) is a different than ${labelize(x)} type (${typeof x}) ${addedMsg ? '(' + addedMsg + ')' : ''}(E: cdb0a3a75b611facfa6d80ff14839323)`); } if (typeof x === 'object') { if (!this.#not) { if (this.value !== x) { const thisValueString = JSON.stringify(this.value); const xString = JSON.stringify(x); if (thisValueString !== xString) { throw new Error(`object comparison using JSON.stringify says these two objects are different. (E: 8375bab01f49dcac280c9dde0e120423)`); } // throw new Error(`clever object comparison is not implemented yet (E: 067c62f1767e25ab39f6aeffccb36e23)`); } } else { if (this.value === x) { throw new Error(`Uh oh. Objects were supposed to be different but they are the same instance of the same object. (E: 3e62e334af53d37686f4009ca8274523)`); } const thisValueString = JSON.stringify(this.value); const xString = JSON.stringify(x); if (thisValueString === xString) { throw new Error(`object comparison using JSON.stringify says these two objects are the same. (E: f661001788a440d08a54bea2e57df580)`); } } } else { if (!this.#not) { if (this.value !== x) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) does not strict equal ${labelize(x)} ${addedMsg ? '(' + addedMsg + ')' : ''}(E: a568969e5729ddb5176e7c76fbdd7b23)`); } } else { if (this.value === x) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) does strict equal ${labelize(x)} ${addedMsg ? '(' + addedMsg + ')' : ''}(E: cc68058d7fa84fbebee8d00cb705df69)`); } } } } catch (error) { // const msg = `${lc} ${error.message}`; const msg = this.getErrorMsgLabeledAndWhatnot(lc, addedMsg, error); this.failMsg = msg; } finally { this.completed = Promise.resolve(true); if (logalot) { console.log(`${lc} complete.`); } } return this; } /** * checks for strict equals ===. * the same as willEqual atow * * @param x value to test against this.value * @param opts * @returns true if passes, false if fails */ isGonnaBe(x: any, opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBe.name}]`; return this.willEqual(x, { ...opts, overrideLc: lc }); } /** * checks for truthy of this.value * * @param opts * @returns true if passes, false if fails */ isGonnaBeTruthy(opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBeTruthy.name}]`; const { logalot } = this; const { addedMsg, justMetaTesting } = opts ?? {}; try { if (logalot) { console.log(`${lc} starting... (I: 8f3e2afedf3b4944bd2d824625ebfb00)`); } this.justMetaTesting = justMetaTesting; if (!this.#not) { if (!this.value) { throw new Error(`Uh oh. Ain't truthy. this.value (${labelize(this.value)}) ${addedMsg ? '(' + addedMsg + ')' : ''}(E: 699e9465b6d64c6fb53a533024751acd)`); } } else { if (this.value) { throw new Error(`Uh oh. It is truthy. this.value (${labelize(this.value)}) ${addedMsg ? '(' + addedMsg + ')' : ''}(E: 061557a7679e4e78ad38d3a9b83c8f8d)`); } } } catch (error) { // const msg = `${lc} ${error.message}`; const msg = this.getErrorMsgLabeledAndWhatnot(lc, addedMsg, error); this.failMsg = msg; } finally { this.completed = Promise.resolve(true); if (logalot) { console.log(`${lc} complete.`); } } return this; } isGonnaBeFalsy(opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBeFalsy.name}]`; // const { logalot } = this; // const { addedMsg, justMetaTesting } = opts ?? {}; return this.not.isGonnaBeTruthy({ ...opts, overrideLc: lc }); } /** * checks for this.value === true (unless not'd) * * @param opts * @returns true if passes, false if fails */ isGonnaBeTrue(opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBeTrue.name}]`; const { logalot } = this; const { addedMsg, justMetaTesting } = opts ?? {}; try { this.justMetaTesting = justMetaTesting; if (logalot) { console.log(`${lc} starting... (I: aade114a8ae7495a9d286a3ebc9be5ba)`); } if (!this.#not) { if (this.value !== true) { throw new Error(`Uh oh. this.value ain't true: (${labelize(this.value)}) ${addedMsg ? '(' + addedMsg + ')' : ''}(E: aeca1221ba994f9eafef8f482e73f941)`); } } else { if (this.value === true) { throw new Error(`Uh oh. It is true. this.value (${labelize(this.value)}) ${addedMsg ? '(' + addedMsg + ')' : ''}(E: cd61a7efa6034c7d94daf9f8f43cdaff)`); } } } catch (error) { // const msg = `${lc} ${error.message}`; const msg = this.getErrorMsgLabeledAndWhatnot(lc, addedMsg, error); this.failMsg = msg; } finally { this.completed = Promise.resolve(true); if (logalot) { console.log(`${lc} complete.`); } } return this; } /** * checks for this.value === false (unless not'd) * * @param opts * @returns true if passes, false if fails */ isGonnaBeFalse(opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBeFalse}]`; return this.willEqual(false, { ...opts, overrideLc: lc }); } /** * checks for this.value === undefined (unless not'd) * * @param opts * @returns true if passes, false if fails */ isGonnaBeUndefined(opts?: ReckoningOptions): Reckoning { const lc = `[${this.isGonnaBeUndefined.name}]`; const { logalot } = this; return this.willEqual(undefined, { ...opts, overrideLc: lc }); // checking this out } /** * checks for this.value.includes(x) (unless not'd) * * @param x value to check for inclusion in this.value * @param opts * @returns true if passes, false if fails */ includes(x: any, opts?: ReckoningOptions): Reckoning { const lc = `[${this.includes.name}]`; const { logalot } = this; const { addedMsg, justMetaTesting } = opts ?? {}; try { if (logalot) { console.log(`${lc} starting... (I: d270ce3e08284de3906447f508f8e912)`); } this.justMetaTesting = justMetaTesting; if (Array.isArray(this.value)) { let arr = this.value as any[]; if (!this.#not) { if (!arr.includes(x)) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) does NOT include ${labelize(x)} (E: c4a17172d5fbe6b637256b1c56c5c823)`); } } else { if (arr.includes(x)) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) DOES include ${labelize(x)} (E: c4a17172d5fbe6b637256b1c56c5c823)`); } } } else if (typeof this.value === 'string') { let valueStr = this.value as string; let xStr = x as string; if (!this.#not) { if (typeof x !== 'string') { throw new Error(`Uh oh. this.value is a string but testing against a non-string inclusion? Terribly sorry to disturb you on this one... (E: 962b88c207b5e701aaa2cb757bd97b23)`); } if (!valueStr.includes(xStr)) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) does NOT include ${labelize(x)} (E: 28b572ece3364c669377f5431a5dcba8)`); } } else { if (valueStr.includes(xStr)) { throw new Error(`Uh oh. this.value (${labelize(this.value)}) DOES include ${labelize(x)} (E: 7c041dd5e43c400fb8edc0460c9a1e4f)`); } } } else { throw new Error(`Uh oh. this.value isn't an array nor a string, if you don't mind me saying. (E: a73a04323478016fc632d8697d695223)`); } } catch (error) { const msg = this.getErrorMsgLabeledAndWhatnot(lc, addedMsg, error); this.failMsg = msg; } finally { this.completed = Promise.resolve(true); if (logalot) { console.log(`${lc} complete.`); } } return this; } // #region helper functions /** * long silly names means I need to refactor them. it's an indicator, conscious decision. eesh. * @param lc log context * @param error catch block error * @returns error msg with extraLabel if assigned */ getErrorMsgLabeledAndWhatnot(lc: string, addedMsg: string | undefined, error: any): string { let msg: string = lc; if (this.extraLabel) { msg += `[${this.extraLabel}]`; } if (addedMsg) { msg += `[${addedMsg}]`; } msg += ` ${error.message}`; return msg; } // #endregion helper functions }