Skip to content

Commit ebab8ba

Browse files
committed
perf(core): add lazy initialization for assert class functions
Improve startup performance by deferring creation of assert function handlers until first use. Previously, all ~100 assertion functions were created eagerly at module load time. Now they are created on-demand using getDeferred(). Core changes: - Refactor assertClass.ts to use lazy initialization via objDefine with `l` (lazy) property instead of `v` (value) - Add expr and not properties to IAssertClassDef for cleaner definition parsing - Improve definition parsing for string, array, and function definitions Documentation: - Add docs/expression-adapter.md with comprehensive guide for createExprAdapter - Document custom assertion patterns, expression syntax, and limitations - Add links from README.md and docs/README.md
1 parent 436d4b7 commit ebab8ba

24 files changed

+1863
-153
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The documentation site includes:
4444
- Feature highlights and comparison tables
4545
- Migration guides (e.g., from Chai.js)
4646
- Detailed guides for specific features (like change/increase/decrease assertions)
47+
- [Expression Adapter Guide](https://nevware21.github.io/tripwire/expression-adapter.html) - Creating custom assertions using expressions
4748
- Links to full TypeDoc API reference for each module
4849

4950
**API Reference (TypeDoc)**:

core/src/assert/assertClass.ts

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {
10-
arrSlice, dumpObj, fnApply, isArray, isFunction, isNullOrUndefined, isString,
11-
isUndefined, objDefine, objForEachKey
10+
arrSlice, dumpObj, fnApply, getDeferred, getLength, ICachedValue, isArray, isFunction, isNullOrUndefined, isString,
11+
isUndefined, objDefine, objForEachKey, strStartsWith
1212
} from "@nevware21/ts-utils";
1313
import { IPromise } from "@nevware21/ts-async";
1414
import { IAssertClass } from "./interface/IAssertClass";
@@ -428,42 +428,62 @@ export function addAssertFunc(target: any, name: string, fnDef: AssertClassDef,
428428
export function addAssertFuncs(target: any, funcs: { [key: string]: AssertClassDef }, responseHandler?: (result: any) => any) {
429429
objForEachKey(funcs, (name, def: AssertClassDef) => {
430430
let theDef: IAssertClassDef;
431-
if (isArray<string>(def)) {
431+
432+
if (isFunction(def)) {
433+
theDef = {
434+
scopeFn: def
435+
};
436+
} else if (isArray<string>(def)) {
432437
if (isString(def[0]) && def.length > 0) {
433438
theDef = {
434-
scopeFn: createExprAdapter(def)
439+
expr: def
435440
};
436-
} else {
437-
throw new AssertionError("Invalid definition for " + name + ": " + def, null, addAssertFuncs);
438441
}
439442
} else if (isString(def)) {
440443
theDef = {
441-
scopeFn: createExprAdapter(def)
442-
};
443-
} else if (isFunction(def)) {
444-
theDef = {
445-
scopeFn: def
444+
expr: def
446445
};
447-
} else if (!def) {
448-
throw new AssertionError("Invalid definition for " + name + ": " + def, null, addAssertFuncs);
449446
} else {
450447
theDef = def;
451448
}
452449

453-
if (!isFunction(theDef) && theDef.alias) {
454-
objDefine(target, name, {
455-
v: _createAliasFunc(theDef.alias)
456-
});
457-
458-
return;
450+
// Sanity check to ensure we have a valid definition (must have at least one of the properties defined)
451+
if (!theDef || (!(isFunction(theDef.scopeFn) || isString(theDef.expr) || isArray<string>(theDef.expr) || theDef.alias))) {
452+
throw new AssertionError("Invalid definition for " + name + ": " + JSON.stringify(theDef), null, addAssertFuncs);
459453
}
460454

461455
objDefine(target, name, {
462-
v: _createProxyFunc(target, name, theDef, addAssertFunc, responseHandler)
456+
l: _createLazyInstHandler(target, name, theDef, addAssertFunc, responseHandler)
463457
});
464458
});
465459
}
466460

461+
function _createLazyInstHandler(target: any, propName: string, theDef: IAssertClassDef, internalErrorStackStart: Function, responseHandler?: (result: any) => any): ICachedValue<(...args: any[]) => void | IAssertInst | IPromise<IAssertInst>> {
462+
return getDeferred(() => {
463+
// See if we can simplify / eliminate the expression definition, if it's just a "not" prefix
464+
if (theDef.expr && isUndefined(theDef.not)) {
465+
if (isString(theDef.expr)) {
466+
if (theDef.expr === "not") {
467+
theDef.not = true;
468+
theDef.expr = null;
469+
} else if (getLength(theDef.expr) > 4 && strStartsWith(theDef.expr, "not.")) {
470+
theDef.not = true;
471+
theDef.expr = theDef.expr.substring(4);
472+
}
473+
} else if (theDef.expr[0] === "not") {
474+
theDef.not = true;
475+
theDef.expr = theDef.expr.length > 1 ? (theDef.expr as string[]).slice(1) : null;
476+
}
477+
}
478+
479+
if (theDef.alias) {
480+
return _createAliasFunc(theDef.alias);
481+
}
482+
483+
return _createProxyFunc(target, propName, theDef, internalErrorStackStart, responseHandler);
484+
});
485+
}
486+
467487
function _extractInitMsg(theArgs: any[], numArgs?: number, mIdx?: number): string {
468488
// Extract the message if its present to be passed to the scope context
469489
let msg: string;
@@ -502,17 +522,27 @@ function _createAliasFunc(alias: string) {
502522
};
503523
}
504524

505-
function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAssertClassDef, internalErrorStackStart: Function, responseHandler: (result: any) => any): IScopeFn {
525+
function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAssertClassDef, internalErrorStackStart: Function, responseHandler: (result: any) => any): (...args: any[]) => void | IAssertInst | IPromise<IAssertInst> {
506526
// let steps: IStepDef[];
507527
let scopeFn: IScopeFn = def.scopeFn;
508528
let mIdx: number = def.mIdx || -1;
509529
let numArgs: number = isNullOrUndefined(def.nArgs) ? 1 : def.nArgs;
510530

531+
if (def.expr && getLength(def.expr) > 0) {
532+
// Convert the expression into a scope function
533+
scopeFn = createExprAdapter(def.expr, scopeFn);
534+
}
535+
536+
if (def.not) {
537+
// Wrap the function in a "not" adapter
538+
scopeFn = createNotAdapter(scopeFn);
539+
}
540+
511541
if (!isFunction(scopeFn)) {
512542
throw new AssertionError("Invalid definition for " + assertName + ": " + dumpObj(def), null, internalErrorStackStart);
513543
}
514544

515-
return function _assertFunc(): any {
545+
return function _assertFunc(): void | IAssertInst | IPromise<IAssertInst> {
516546
let theArgs = arrSlice(arguments, 0);
517547
let orgArgs = arrSlice(theArgs);
518548

@@ -539,7 +569,7 @@ function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAss
539569
v: newScope.context
540570
});
541571

542-
let theResult = newScope.exec(scopeFn, scopeArgs || theArgs, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous");
572+
let theResult = newScope.exec(scopeFn, scopeArgs || theArgs, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous") as void | IAssertInst | IPromise<IAssertInst>;
543573

544574
return responseHandler ? responseHandler(theResult) : theResult;
545575
};

core/src/assert/interface/IAssertClass.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export interface IAssertClass<AssertInst extends IAssertInst = IAssertInst> {
7373
* @example
7474
* ```typescript
7575
* assert.fail(); // Throws AssertionFailure
76-
* assert.fail("Hello Darkness, my old friend"); // Throws AssertionFailure
76+
* assert.fail("Hello silence, my quiet friend"); // Throws AssertionFailure
7777
* assert.fail(() => "Looks like we have failed again"); // Throws AssertionFailure
7878
* ```
7979
*/
@@ -103,7 +103,7 @@ export interface IAssertClass<AssertInst extends IAssertInst = IAssertInst> {
103103
* @example
104104
* ```typescript
105105
* assert.fatal(); // Throws AssertionFatal
106-
* assert.fatal("Hello Darkness, my old friend"); // Throws AssertionFatal
106+
* assert.fatal("Hello silence, my quiet friend"); // Throws AssertionFatal
107107
* assert.fatal(() => "Looks like we have failed again"); // Throws AssertionFatal
108108
* ```
109109
*/

core/src/assert/interface/IAssertClassDef.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ export interface IAssertClassDef {
2323
*/
2424
alias?: string;
2525

26+
/**
27+
* Identifies the expression or path that is to be evaluated to obtain the value
28+
* that is to be asserted against. This expression is evaluated against the
29+
* current scope value (`this.context.value`) when the assertion is evaluated.
30+
* This expression can either be a `string` using dot notation to identify
31+
* the path to be evaluated or an array of `string`s where each entry in the
32+
* array identifies a step in the path to be evaluated.
33+
* For example:
34+
* - `"myProperty.nestedProperty[0].value"` or
35+
* - `["myProperty", "nestedProperty", "{0}", "value"]`
36+
*
37+
* When using an array of `string`s each entry in the array is treated as a
38+
* step in the evaluation path, this enables more complex paths to be defined
39+
* where property names may contain characters that would otherwise be
40+
* interpreted as part of the path syntax, for example a property name
41+
* that contains a dot (`.`) character.
42+
* In addition, when using an array of `string`s, it enables the use
43+
* of "dynamic" property names or indexes to be used via the use of the
44+
* `{}` syntax. This syntax enables either a named value or an index
45+
* to be specified which will be resolved against the current scope
46+
* context's named values or the arguments that were passed to the
47+
* assertion function.
48+
* - Named values are resolved against the current scope context's
49+
* named values collection (`this.context.namedValues`).
50+
* - Indexed values are resolved against the arguments that were
51+
* passed to the assertion function, where `{0}` identifies the
52+
* first argument, `{1}` the second argument and so on.
53+
*/
54+
expr?: string | string[];
55+
56+
/**
57+
* Indicates that the evaluation should be negated (i.e. NOT). This is equivalent to
58+
* wrapping the evaluation within a logical NOT operation.
59+
* For example if the evaluation would normally return `true` it would
60+
* instead return `false` and vice versa.
61+
*/
62+
not?: boolean;
63+
2664
/**
2765
* Identifies an {@link IScopeFn} function that will be called, this function
2866
* expects the current scope as the `this` value and any additional arguments

core/src/assert/interface/IAssertInst.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export interface IAssertInst extends INotOp<IAssertInst>, IAssertInstCore, IEqua
144144
* import { assert } from "@nevware21/tripwire";
145145
*
146146
* const arr = [1, 2, 3];
147-
* const str = "hello darkness welcome back again";
147+
* const str = "hello silence welcome back again";
148148
* const obj = { a: 1, b: 2, c: 3 };
149149
*
150150
* assert(arr).include(2); // Passes
@@ -170,7 +170,7 @@ export interface IAssertInst extends INotOp<IAssertInst>, IAssertInstCore, IEqua
170170
* import { assert } from "@nevware21/tripwire";
171171
*
172172
* const arr = [1, 2, 3];
173-
* const str = "hello darkness welcome back again";
173+
* const str = "hello silence welcome back again";
174174
* const obj = { a: 1, b: 2, c: 3 };
175175
*
176176
* assert(arr).includes(2); // Passes
@@ -195,7 +195,7 @@ export interface IAssertInst extends INotOp<IAssertInst>, IAssertInstCore, IEqua
195195
* import { assert } from "@nevware21/tripwire";
196196
*
197197
* const arr = [1, 2, 3];
198-
* const str = "hello darkness welcome back again";
198+
* const str = "hello silence welcome back again";
199199
* const obj = { a: 1, b: 2, c: 3 };
200200
*
201201
* assert(arr).contain(2); // Passes
@@ -220,7 +220,7 @@ export interface IAssertInst extends INotOp<IAssertInst>, IAssertInstCore, IEqua
220220
* import { assert } from "@nevware21/tripwire";
221221
*
222222
* const arr = [1, 2, 3];
223-
* const str = "hello darkness welcome back again";
223+
* const str = "hello silence welcome back again";
224224
* const obj = { a: 1, b: 2, c: 3 };
225225
*
226226
* assert(arr).contains(2); // Passes

core/src/assert/interface/ops/IToOp.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ export interface IToOp<R> extends INotOp<IToOp<R>> {
101101
* import { assert } from "@nevware21/tripwire";
102102
*
103103
* const arr = [1, 2, 3];
104-
* const str = "hello darkness welcome back again";
104+
* const str = "hello silence welcome back again";
105105
* const obj = { a: 1, b: 2, c: 3 };
106106
*
107107
* assert(arr).include(2); // Passes
108-
* assert(str).include("darkness"); // Passes
108+
* assert(str).include("silence"); // Passes
109109
* assert(obj).include.all.keys('a', 'b'); // Passes
110110
* assert(arr).include(4); // Fails
111111
* assert(str).include("planet"); // Fails
@@ -127,11 +127,11 @@ export interface IToOp<R> extends INotOp<IToOp<R>> {
127127
* import { assert } from "@nevware21/tripwire";
128128
*
129129
* const arr = [1, 2, 3];
130-
* const str = "hello darkness welcome back again";
130+
* const str = "hello silence welcome back again";
131131
* const obj = { a: 1, b: 2, c: 3 };
132132
*
133133
* assert(arr).includes(2); // Passes
134-
* assert(str).includes("darkness"); // Passes
134+
* assert(str).includes("silence"); // Passes
135135
* assert(obj).includes.all.keys('a', 'b'); // Passes
136136
* assert(arr).includes(4); // Fails
137137
* assert(str).includes("planet"); // Fails
@@ -152,11 +152,11 @@ export interface IToOp<R> extends INotOp<IToOp<R>> {
152152
* import { assert } from "@nevware21/tripwire";
153153
*
154154
* const arr = [1, 2, 3];
155-
* const str = "hello darkness welcome back again";
155+
* const str = "hello silence welcome back again";
156156
* const obj = { a: 1, b: 2, c: 3 };
157157
*
158158
* assert(arr).contain(2); // Passes
159-
* assert(str).contain("darkness"); // Passes
159+
* assert(str).contain("silence"); // Passes
160160
* assert(obj).contain.all.keys('a', 'b'); // Passes
161161
* assert(arr).contain(4); // Fails
162162
* assert(str).contain("planet"); // Fails
@@ -177,11 +177,11 @@ export interface IToOp<R> extends INotOp<IToOp<R>> {
177177
* import { assert } from "@nevware21/tripwire";
178178
*
179179
* const arr = [1, 2, 3];
180-
* const str = "hello darkness welcome back again";
180+
* const str = "hello silence welcome back again";
181181
* const obj = { a: 1, b: 2, c: 3 };
182182
*
183183
* assert(arr).contains(2); // Passes
184-
* assert(str).contains("darkness"); // Passes
184+
* assert(str).contains("silence"); // Passes
185185
* assert(obj).contains.all.keys('a', 'b'); // Passes
186186
* assert(arr).contains(4); // Fails
187187
* assert(str).contains("planet"); // Fails

0 commit comments

Comments
 (0)