xdbc 1.0.217 → 1.0.219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.gitattributes +16 -8
  2. package/.vscode/settings.json +3 -3
  3. package/.vscode/tasks.json +23 -23
  4. package/ASSESSMENT.md +249 -0
  5. package/README.md +538 -408
  6. package/__tests__/DBC/AE.test.ts +62 -62
  7. package/__tests__/DBC/ARRAY.test.ts +91 -91
  8. package/__tests__/DBC/DEFINED.test.ts +53 -53
  9. package/__tests__/DBC/DOM.test.ts +786 -0
  10. package/__tests__/DBC/Decorators.test.ts +367 -367
  11. package/__tests__/DBC/EQ.test.ts +13 -13
  12. package/__tests__/DBC/GREATER.test.ts +31 -31
  13. package/__tests__/DBC/HasAttribute.test.ts +60 -60
  14. package/__tests__/DBC/IF.test.ts +62 -62
  15. package/__tests__/DBC/INSTANCE.test.ts +13 -13
  16. package/__tests__/DBC/JSON.OP.test.ts +47 -47
  17. package/__tests__/DBC/JSON.Parse.test.ts +17 -17
  18. package/__tests__/DBC/OR.test.ts +14 -14
  19. package/__tests__/DBC/PLAIN_OBJECT.test.ts +109 -109
  20. package/__tests__/DBC/REGEX.test.ts +17 -17
  21. package/__tests__/DBC/TYPE.test.ts +13 -13
  22. package/__tests__/DBC/UNDEFINED.test.ts +45 -45
  23. package/__tests__/DBC/ZOD.test.ts +54 -54
  24. package/__tests__/DBC/onInfringement.test.ts +262 -0
  25. package/biome.json +45 -40
  26. package/dist/DBC/AE.js +172 -0
  27. package/dist/DBC/ARR/PLAIN_OBJECT.d.ts +0 -3
  28. package/dist/DBC/ARR/PLAIN_OBJECT.js +95 -0
  29. package/dist/DBC/ARRAY.d.ts +0 -3
  30. package/dist/DBC/ARRAY.js +90 -0
  31. package/dist/DBC/COMPARISON/GREATER.js +21 -0
  32. package/dist/DBC/COMPARISON/GREATER_OR_EQUAL.js +21 -0
  33. package/dist/DBC/COMPARISON/LESS.js +21 -0
  34. package/dist/DBC/COMPARISON/LESS_OR_EQUAL.js +21 -0
  35. package/dist/DBC/COMPARISON.js +98 -0
  36. package/dist/DBC/DEFINED.js +87 -0
  37. package/dist/DBC/DOM.d.ts +123 -0
  38. package/dist/DBC/DOM.js +362 -0
  39. package/dist/DBC/EQ/DIFFERENT.js +34 -0
  40. package/dist/DBC/EQ.js +101 -0
  41. package/dist/DBC/GREATER.js +99 -0
  42. package/dist/DBC/HasAttribute.js +101 -0
  43. package/dist/DBC/IF.js +96 -0
  44. package/dist/DBC/INSTANCE.js +122 -0
  45. package/dist/DBC/JSON.OP.js +120 -0
  46. package/dist/DBC/JSON.Parse.js +104 -0
  47. package/dist/DBC/OR.js +125 -0
  48. package/dist/DBC/REGEX.js +136 -0
  49. package/dist/DBC/TYPE.js +112 -0
  50. package/dist/DBC/UNDEFINED.js +87 -0
  51. package/dist/DBC/ZOD.js +99 -0
  52. package/dist/DBC.d.ts +18 -4
  53. package/dist/DBC.js +645 -0
  54. package/dist/Demo.d.ts +10 -0
  55. package/dist/Demo.js +713 -0
  56. package/dist/Test.html +18 -0
  57. package/dist/bundle.js +6140 -405
  58. package/dist/index.d.ts +22 -0
  59. package/dist/index.html +18 -0
  60. package/dist/index.js +22 -0
  61. package/docs/assets/highlight.css +22 -22
  62. package/docs/assets/icons.js +17 -17
  63. package/docs/assets/main.js +60 -60
  64. package/docs/assets/style.css +1640 -1640
  65. package/docs/classes/DBC.DBC.html +98 -98
  66. package/docs/classes/DBC_AE.AE.html +160 -160
  67. package/docs/classes/DBC_EQ.EQ.html +131 -131
  68. package/docs/classes/DBC_GREATER.GREATER.html +139 -139
  69. package/docs/classes/DBC_INSTANCE.INSTANCE.html +130 -130
  70. package/docs/classes/DBC_JSON.OP.JSON_OP.html +138 -138
  71. package/docs/classes/DBC_JSON.Parse.JSON_Parse.html +129 -129
  72. package/docs/classes/DBC_OR.OR.html +137 -137
  73. package/docs/classes/DBC_REGEX.REGEX.html +136 -136
  74. package/docs/classes/DBC_TYPE.TYPE.html +130 -130
  75. package/docs/classes/Demo.Demo.html +14 -14
  76. package/docs/hierarchy.html +1 -1
  77. package/docs/index.html +1 -1
  78. package/docs/modules/DBC.html +1 -1
  79. package/docs/modules/DBC_AE.html +1 -1
  80. package/docs/modules/DBC_EQ.html +1 -1
  81. package/docs/modules/DBC_GREATER.html +1 -1
  82. package/docs/modules/DBC_INSTANCE.html +1 -1
  83. package/docs/modules/DBC_JSON.OP.html +1 -1
  84. package/docs/modules/DBC_JSON.Parse.html +1 -1
  85. package/docs/modules/DBC_OR.html +1 -1
  86. package/docs/modules/DBC_REGEX.html +1 -1
  87. package/docs/modules/DBC_TYPE.html +1 -1
  88. package/docs/modules/Demo.html +1 -1
  89. package/jest.config.js +32 -32
  90. package/package.json +71 -55
  91. package/src/DBC/AE.ts +269 -288
  92. package/src/DBC/ARR/PLAIN_OBJECT.ts +122 -133
  93. package/src/DBC/ARRAY.ts +117 -127
  94. package/src/DBC/COMPARISON/GREATER.ts +41 -46
  95. package/src/DBC/COMPARISON/GREATER_OR_EQUAL.ts +41 -45
  96. package/src/DBC/COMPARISON/LESS.ts +41 -45
  97. package/src/DBC/COMPARISON/LESS_OR_EQUAL.ts +41 -45
  98. package/src/DBC/COMPARISON.ts +149 -159
  99. package/src/DBC/DEFINED.ts +117 -122
  100. package/src/DBC/DOM.ts +453 -0
  101. package/src/DBC/EQ/DIFFERENT.ts +51 -57
  102. package/src/DBC/EQ.ts +154 -163
  103. package/src/DBC/HasAttribute.ts +149 -154
  104. package/src/DBC/IF.ts +173 -179
  105. package/src/DBC/INSTANCE.ts +168 -171
  106. package/src/DBC/JSON.OP.ts +178 -186
  107. package/src/DBC/JSON.Parse.ts +150 -157
  108. package/src/DBC/OR.ts +183 -187
  109. package/src/DBC/REGEX.ts +195 -196
  110. package/src/DBC/TYPE.ts +142 -149
  111. package/src/DBC/UNDEFINED.ts +115 -117
  112. package/src/DBC/ZOD.ts +130 -135
  113. package/src/DBC.ts +902 -904
  114. package/src/Demo.ts +537 -404
  115. package/src/index.ts +22 -0
  116. package/tsconfig.json +18 -18
  117. package/tsconfig.test.json +7 -7
  118. package/typedoc.json +16 -16
  119. package/webpack.config.js +27 -27
  120. package/Assessment.md +0 -507
package/src/DBC.ts CHANGED
@@ -1,904 +1,902 @@
1
- /**
2
- * Provides a **D**esign **B**y **C**ontract Framework using decorators.
3
- *
4
- * @remarks
5
- * Maintainer: Callari, Salvatore (XDBC@WaXCode.net) */
6
- export class DBC {
7
- // #region Internal caches.
8
- private static readonly MAX_CACHE_SIZE = 1000;
9
- private static dbcCache: Map<string, DBC> = new Map();
10
- private static pathTokenCache: Map<string, string[]> = new Map();
11
- /** Evicts the oldest entry if the cache exceeds the maximum size. */
12
- private static evictIfNeeded<K, V>(cache: Map<K, V>): void {
13
- if (cache.size > DBC.MAX_CACHE_SIZE) {
14
- const oldest = cache.keys().next().value;
15
- if (oldest !== undefined) cache.delete(oldest);
16
- }
17
- }
18
- private static getHost(): unknown {
19
- return typeof window !== "undefined" ? window : globalThis;
20
- }
21
- private static getDBC(dbc: string | DBC | undefined): DBC {
22
- if (dbc instanceof DBC) return dbc;
23
- const path = dbc ?? "WaXCode.DBC";
24
- if (DBC.dbcCache.has(path)) {
25
- return DBC.dbcCache.get(path) as DBC;
26
- }
27
- const resolved = DBC.resolveDBCPath(DBC.getHost(), path);
28
- if (resolved) {
29
- DBC.evictIfNeeded(DBC.dbcCache);
30
- DBC.dbcCache.set(path, resolved);
31
- return resolved;
32
- }
33
- throw new Error(
34
- `[XDBC] DBC instance not found at path "${path}". Ensure a DBC instance is registered there.`,
35
- );
36
- }
37
- // #endregion Internal caches.
38
- // #region Parameter-value requests.
39
- /** Stores all request for parameter values registered by {@link decPrecondition }. */
40
- static paramValueRequests: Map<
41
- string,
42
- // biome-ignore lint/suspicious/noExplicitAny: Gotta be any since parameter-values may be undefined.
43
- Map<number, Array<(value: any) => undefined>>
44
- > = new Map<
45
- string,
46
- // biome-ignore lint/suspicious/noExplicitAny: Gotta be any since parameter-values may be undefined.
47
- Map<number, Array<(value: any) => undefined>>
48
- >();
49
- /**
50
- * Generate a unique key for storing parameter value requests.
51
- * Format: "ClassName:methodName"
52
- */
53
- private static getRequestKey(
54
- target: object,
55
- methodName: string | symbol | undefined,
56
- ): string {
57
- const className =
58
- typeof target === "function"
59
- ? target.name
60
- : target.constructor?.name || "Unknown";
61
- return `${className}:${String(methodName)}`;
62
- }
63
- /**
64
- * Make a request to get the value of a certain parameter of specific method in a specific {@link object }.
65
- * That request gets enlisted in {@link paramValueRequests } which is used by {@link ParamvalueProvider} to invoke the
66
- * given "receptor" with the parameter value stored in there. Thus a parameter decorator using this method will
67
- * not receive any value of the top method is not tagged with {@link ParamvalueProvider}.
68
- *
69
- * @param target The {@link object } containing the method with the parameter which's value is requested.
70
- * @param methodName The name of the method with the parameter which's value is requested.
71
- * @param index The index of the parameter which's value is requested.
72
- * @param receptor The method the requested parameter-value shall be passed to when it becomes available. */
73
- protected static requestParamValue(
74
- target: object,
75
- methodName: string | symbol | undefined,
76
- index: number,
77
- // biome-ignore lint/suspicious/noExplicitAny: Gotta be any since parameter-values may be undefined.
78
- receptor: (value: any) => undefined,
79
- ): undefined {
80
- const key = DBC.getRequestKey(target, methodName);
81
-
82
- if (DBC.paramValueRequests.has(key)) {
83
- const paramMap = DBC.paramValueRequests.get(key)!;
84
- if (paramMap.has(index)) {
85
- paramMap.get(index)?.push(receptor);
86
- } else {
87
- paramMap.set(index, new Array<(value: unknown) => undefined>(receptor));
88
- }
89
- } else {
90
- DBC.paramValueRequests.set(
91
- key,
92
- new Map<number, Array<(value: unknown) => undefined>>([
93
- [index, new Array<(value: unknown) => undefined>(receptor)],
94
- ]),
95
- );
96
- }
97
-
98
- return undefined;
99
- }
100
- /**
101
- * A decorator usable on both **methods** (including setters) and **classes**.
102
- *
103
- * - **On a method**: wraps the method so that any {@link DBC } preconditions registered on its parameters
104
- * via XDBC parameter-decorator factories are dispatched with the actual argument values on each call.
105
- * - **On a class**: wraps the constructor so that preconditions registered on constructor parameters are
106
- * dispatched **before** the original constructor body runs, upholding true precondition semantics.
107
- *
108
- * In both cases the "receptor" callbacks enlisted in {@link paramValueRequests } by {@link requestParamValue }
109
- * are invoked with the live argument values.
110
- *
111
- * @param targetOrConstructor When used as a **method** decorator: the {@link object } hosting the tagged method.
112
- * When used as a **class** decorator: the class constructor.
113
- * @param propertyKey *(method decorator only)* The tagged method's name as provided by the runtime.
114
- * @param descriptor *(method decorator only)* The {@link PropertyDescriptor } as provided by the runtime.
115
- *
116
- * @returns When used as a **method** decorator: the (modified) {@link PropertyDescriptor }.
117
- * When used as a **class** decorator: a replacement constructor that performs precondition checks. */
118
- // biome-ignore lint/suspicious/noExplicitAny: Must handle both method and class decorator signatures
119
- public static ParamvalueProvider(target: object, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
120
- // biome-ignore lint/suspicious/noExplicitAny: Must handle both method and class decorator signatures
121
- public static ParamvalueProvider<T extends new (...args: any[]) => any>(constructor: T): T;
122
- // biome-ignore lint/suspicious/noExplicitAny: Must accept abstract class constructors
123
- public static ParamvalueProvider<T extends abstract new (...args: any[]) => any>(constructor: T): T;
124
- // biome-ignore lint/suspicious/noExplicitAny: Must handle both method and class decorator signatures
125
- public static ParamvalueProvider(...args: any[]): any {
126
- if (args.length === 1 && typeof args[0] === "function") {
127
- // #region Class decorator path
128
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any constructor signature
129
- const constructor = args[0] as new (...args: any[]) => any;
130
- const key = `${constructor.name}:undefined`;
131
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any constructor signature
132
- const WrappedClass = class extends constructor {
133
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any constructor signature
134
- constructor(...ctorArgs: any[]) {
135
- if (DBC.paramValueRequests.has(key)) {
136
- const paramMap = DBC.paramValueRequests.get(key)!;
137
- for (const index of paramMap.keys()) {
138
- if (index < ctorArgs.length) {
139
- for (const receptor of paramMap.get(index)!) {
140
- receptor(ctorArgs[index]);
141
- }
142
- }
143
- }
144
- }
145
- super(...ctorArgs);
146
- }
147
- } as typeof constructor;
148
- Object.defineProperty(WrappedClass, "name", { value: constructor.name });
149
- return WrappedClass;
150
- // #endregion Class decorator path
151
- }
152
-
153
- // #region Method decorator path
154
- const [target, propertyKey, descriptor] = args as [object, string, PropertyDescriptor];
155
- const originalMethod = descriptor.value;
156
- const isStatic = typeof target === "function";
157
- // biome-ignore lint/suspicious/noExplicitAny: Gotta be any since parameter-values may be undefined.
158
- descriptor.value = function (...methodArgs: any[]) {
159
- // #region Check if a value of one of the method's parameter has been requested and pass it to the
160
- // receptor, if so.
161
- const actualTarget = isStatic ? this : (this as any).constructor;
162
- const key = DBC.getRequestKey(actualTarget, propertyKey);
163
-
164
- if (DBC.paramValueRequests.has(key)) {
165
- const paramMap = DBC.paramValueRequests.get(key)!;
166
- for (const index of paramMap.keys()) {
167
- if (index < methodArgs.length) {
168
- for (const receptor of paramMap.get(index)!) {
169
- receptor(methodArgs[index]);
170
- }
171
- }
172
- }
173
- } else {
174
- console.warn("No parameter value requests found for key:", key);
175
- }
176
- // #endregion Check if a value of one of the method's parameter has been requested and pass it to the
177
- // receptor, if so.
178
- return originalMethod.apply(this, methodArgs);
179
- };
180
- return descriptor;
181
- // #endregion Method decorator path
182
- }
183
- // #endregion Parameter-value requests.
184
- // #region Class
185
- /**
186
- * A property-decorator factory serving as a **D**esign **B**y **C**ontract Invariant.
187
- * This invariant aims to check the instance of the class not the value to be get or set.
188
- *
189
- * @param contracts The {@link DBC }-Contracts the value shall uphold.
190
- *
191
- * @throws A {@link DBC.Infringement } whenever the property is tried to be get or set without the instance of it's class
192
- * fulfilling the specified **contracts**. */
193
- public static decClassInvariant(
194
- contracts: Array<{
195
- check: (toCheck: unknown | null | undefined) => boolean | string;
196
- }>,
197
- path: string | undefined = undefined,
198
- dbc = "WaXCode.DBC",
199
- ) {
200
- let dbcInstance: DBC | undefined;
201
- return (
202
- target: unknown,
203
- propertyKey: string | symbol,
204
- descriptor: PropertyDescriptor,
205
- ) => {
206
- if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
207
- if (!dbcInstance.executionSettings.checkInvariants) {
208
- return;
209
- }
210
- const originalSetter = descriptor.set;
211
- const originalGetter = descriptor.get;
212
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to intercept UNDEFINED and NULL.
213
- let value: any;
214
- // #region Replace original property.
215
- Object.defineProperty(target, propertyKey, {
216
- get() {
217
- if (!dbcInstance?.executionSettings.checkInvariants) {
218
- return;
219
- }
220
-
221
- const realValue = path ? DBC.resolve(this, path) : this;
222
- // #region Check if all "contracts" are fulfilled.
223
- for (const contract of contracts) {
224
- const result = contract.check(realValue);
225
-
226
- if (typeof result === "string") {
227
- dbcInstance?.reportFieldInfringement(
228
- result,
229
- target as object,
230
- path,
231
- propertyKey as string,
232
- realValue,
233
- );
234
- }
235
- }
236
- // #endregion Check if all "contracts" are fulfilled.
237
- return originalGetter ? (originalGetter as any)[propertyKey] : value;
238
- },
239
- set(newValue) {
240
- if (!dbcInstance?.executionSettings.checkInvariants) {
241
- return;
242
- }
243
-
244
- const realValue = path ? DBC.resolve(this, path) : this;
245
- // #region Check if all "contracts" are fulfilled.
246
- for (const contract of contracts) {
247
- const result = contract.check(realValue);
248
-
249
- if (typeof result === "string") {
250
- dbcInstance?.reportFieldInfringement(
251
- result,
252
- target as object,
253
- path,
254
- propertyKey as string,
255
- realValue,
256
- );
257
- }
258
- }
259
- // #endregion Check if all "contracts" are fulfilled.
260
- value = newValue;
261
- },
262
- enumerable: true,
263
- configurable: true,
264
- });
265
- // #endregion Replace original property.
266
- };
267
- }
268
- // #endregion Class
269
- // #region Invariant
270
- /**
271
- * A property-decorator factory serving as a **D**esign **B**y **C**ontract Invariant.
272
- * Since the value must be initialized or set according to the specified **contracts** the value will only be checked
273
- * when assigning it.
274
- *
275
- * @param contracts The {@link DBC }-Contracts the value shall uphold.
276
- *
277
- * @throws A {@link DBC.Infringement } whenever the property is tried to be set to a value that does not comply to the
278
- * specified **contracts**, by the returned method.*/
279
- public static decInvariant(
280
- contracts: Array<{
281
- check: (toCheck: unknown | null | undefined) => boolean | string;
282
- }>,
283
- path: string | undefined = undefined,
284
- dbc: string | undefined = undefined,
285
- hint: string | undefined = undefined,
286
- ) {
287
- let dbcInstance: DBC | undefined;
288
- return (target: unknown, propertyKey: string | symbol) => {
289
- if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
290
- if (!dbcInstance.executionSettings.checkInvariants) {
291
- return;
292
- }
293
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to intercept UNDEFINED and NULL.
294
- let value: any;
295
- // #region Replace original property.
296
- Object.defineProperty(target, propertyKey, {
297
- set(newValue) {
298
- if (!dbcInstance?.executionSettings.checkInvariants) {
299
- return;
300
- }
301
-
302
- const realValue = path ? DBC.resolve(newValue, path) : newValue;
303
- // #region Check if all "contracts" are fulfilled.
304
- for (const contract of contracts) {
305
- const result = contract.check(realValue);
306
-
307
- if (typeof result === "string") {
308
- dbcInstance?.reportFieldInfringement(
309
- result,
310
- target as object,
311
- path,
312
- propertyKey as string,
313
- realValue,
314
- hint,
315
- );
316
- }
317
- }
318
- // #endregion Check if all "contracts" are fulfilled.
319
- value = newValue;
320
- },
321
- enumerable: true,
322
- configurable: true,
323
- });
324
- // #endregion Replace original property.
325
- };
326
- }
327
- // #endregion Invariant
328
- // #region Postcondition
329
- /**
330
- * A method decorator factory checking the result of a method whenever it is invoked thus also usable on getters.
331
- *
332
- * @param check The **(toCheck: any, object, string) => boolean | string** to use for checking.
333
- * @param dbc See {@link DBC.resolveDBCPath }.
334
- * @param path The dotted path referring to the actual value to check, starting form the specified one.
335
- *
336
- * @returns The **( target : object, propertyKey : string, descriptor : PropertyDescriptor ) : PropertyDescriptor**
337
- * invoked by Typescript.
338
- */
339
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to intercept UNDEFINED and NULL.
340
- public static decPostcondition(
341
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to intercept UNDEFINED and NULL.
342
- check: (
343
- toCheck: any,
344
- target: object,
345
- propertyKey: string,
346
- ) => boolean | string,
347
- dbc: string | undefined = undefined,
348
- path: string | undefined = undefined,
349
- hint: string | undefined = undefined,
350
- ) {
351
- let dbcInstance: DBC | undefined;
352
- return (
353
- target: object,
354
- propertyKey: string,
355
- descriptor: PropertyDescriptor,
356
- ): PropertyDescriptor => {
357
- const originalMethod = descriptor.value;
358
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to intercept UNDEFINED and NULL.
359
- descriptor.value = (...args: any[]) => {
360
- if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
361
- if (!dbcInstance.executionSettings.checkPostconditions) {
362
- return;
363
- }
364
- // biome-ignore lint/complexity/noThisInStatic: <explanation>
365
- const result = originalMethod.apply(this, args);
366
- const realValue = path ? DBC.resolve(result, path) : result;
367
- const checkResult = check(realValue, target, propertyKey);
368
-
369
- if (typeof checkResult === "string") {
370
- dbcInstance.reportReturnvalueInfringement(
371
- checkResult,
372
- target,
373
- path,
374
- propertyKey,
375
- realValue,
376
- hint,
377
- );
378
- }
379
-
380
- return result;
381
- };
382
-
383
- return descriptor;
384
- };
385
- }
386
- // #endregion Postcondition
387
- // #region Decorator
388
- // #region Precondition
389
- /**
390
- * A parameter-decorator factory that requests the tagged parameter's value passing it to the provided
391
- * "check"-method when the value becomes available.
392
- *
393
- * @param check The "( unknown ) => void" to be invoked along with the tagged parameter's value as soon
394
- * as it becomes available.
395
- * @param dbc See {@link DBC.resolveDBCPath }.
396
- * @param path The dotted path referring to the actual value to check, starting form the specified one.
397
- * May contain :: to separate multiple paths.
398
- *
399
- * @returns The **(target: object, methodName: string | symbol, parameterIndex: number ) => void** invoked by Typescript- */
400
- protected static decPrecondition(
401
- // biome-ignore lint/suspicious/noExplicitAny: Necessary to check any parameter value
402
- check: (
403
- value: unknown,
404
- target: object,
405
- methodName: string | symbol | undefined,
406
- parameterIndex: number,
407
- ) => boolean | string,
408
- dbc: string | undefined = undefined,
409
- path: string | undefined = undefined,
410
- hint: string | undefined = undefined,
411
- ): (
412
- target: object,
413
- methodName: string | symbol | undefined,
414
- parameterIndex: number,
415
- ) => void {
416
- const paths = path ? path.replace(/ /g, "").split("::") : [undefined];
417
- let dbcInstance: DBC | undefined;
418
- return (
419
- target: object,
420
- methodName: string | symbol | undefined,
421
- parameterIndex: number,
422
- ): void => {
423
- DBC.requestParamValue(
424
- target,
425
- methodName,
426
- parameterIndex,
427
- (value: unknown) => {
428
- if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
429
- if (!dbcInstance.executionSettings.checkPreconditions) {
430
- return;
431
- }
432
-
433
- for (const singlePath of paths) {
434
- const realValue = singlePath
435
- ? DBC.resolve(value, singlePath)
436
- : value;
437
- const result = check(realValue, target, methodName, parameterIndex);
438
-
439
- if (typeof result === "string") {
440
- dbcInstance.reportParameterInfringement(
441
- result,
442
- target,
443
- singlePath,
444
- methodName as string,
445
- parameterIndex,
446
- realValue,
447
- hint,
448
- );
449
- }
450
- }
451
- },
452
- );
453
- };
454
- }
455
- // #endregion Precondition
456
- // #endregion Decorator
457
- // #region Contract Factory Helpers
458
- /**
459
- * Creates a PRE decorator from a checkAlgorithm function and its bound arguments.
460
- * Reduces boilerplate across contract classes.
461
- *
462
- * @param checkFn A function that takes (value, ...boundArgs) and returns true or an error string.
463
- * @param boundArgs The arguments to bind to the check function after the value.
464
- * @param dbc See {@link DBC.decPrecondition}.
465
- * @param path See {@link DBC.decPrecondition}.
466
- * @param hint See {@link DBC.decPrecondition}.
467
- */
468
- public static createPRE(
469
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any checkAlgorithm signature
470
- checkFn: (...args: any[]) => boolean | string,
471
- // biome-ignore lint/suspicious/noExplicitAny: Arguments vary per contract
472
- boundArgs: any[],
473
- dbc?: string,
474
- path?: string,
475
- hint?: string,
476
- ) {
477
- return DBC.decPrecondition(
478
- (value, _target, _methodName, _parameterIndex) => {
479
- return checkFn(value, ...boundArgs);
480
- },
481
- dbc,
482
- path,
483
- hint,
484
- );
485
- }
486
- /**
487
- * Creates a POST decorator from a checkAlgorithm function and its bound arguments.
488
- *
489
- * @param checkFn A function that takes (value, ...boundArgs) and returns true or an error string.
490
- * @param boundArgs The arguments to bind to the check function after the value.
491
- * @param dbc See {@link DBC.decPostcondition}.
492
- * @param path See {@link DBC.decPostcondition}.
493
- * @param hint See {@link DBC.decPostcondition}.
494
- */
495
- public static createPOST(
496
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any checkAlgorithm signature
497
- checkFn: (...args: any[]) => boolean | string,
498
- // biome-ignore lint/suspicious/noExplicitAny: Arguments vary per contract
499
- boundArgs: any[],
500
- dbc?: string,
501
- path?: string,
502
- hint?: string,
503
- ) {
504
- return DBC.decPostcondition(
505
- (value, _target, _propertyKey) => {
506
- return checkFn(value, ...boundArgs);
507
- },
508
- dbc,
509
- path,
510
- hint,
511
- );
512
- }
513
- /**
514
- * Creates an INVARIANT decorator from a contract constructor and its bound arguments.
515
- *
516
- * @param ContractClass A class with a constructor that produces an object with a `check` method.
517
- * @param ctorArgs The arguments to pass to the contract constructor.
518
- * @param dbc See {@link DBC.decInvariant}.
519
- * @param path See {@link DBC.decInvariant}.
520
- * @param hint See {@link DBC.decInvariant}.
521
- */
522
- public static createINVARIANT(
523
- // biome-ignore lint/suspicious/noExplicitAny: Must accept any contract constructor
524
- ContractClass: new (
525
- ...args: any[]
526
- ) => { check: (toCheck: unknown | null | undefined) => boolean | string },
527
- // biome-ignore lint/suspicious/noExplicitAny: Arguments vary per contract
528
- ctorArgs: any[],
529
- dbc?: string,
530
- path?: string,
531
- hint?: string,
532
- ) {
533
- return DBC.decInvariant([new ContractClass(...ctorArgs)], path, dbc, hint);
534
- }
535
- // #endregion Contract Factory Helpers
536
- // #region Execution Handling
537
- /** Stores settings concerning the execution of checks. */
538
- public executionSettings: {
539
- checkPreconditions: boolean;
540
- checkPostconditions: boolean;
541
- checkInvariants: boolean;
542
- } = {
543
- checkPreconditions: true,
544
- checkPostconditions: true,
545
- checkInvariants: true,
546
- };
547
- // #endregion Execution Handling
548
- // #region Warning handling.
549
- /** Stores settings concerning warnings. */
550
- public warningSettings: {
551
- logToConsole: boolean;
552
- } = { logToConsole: true };
553
- /**
554
- * Reports a warning.
555
- *
556
- * @param message The message containing the warning. */
557
- protected reportWarning(message: string): undefined {
558
- if (this.warningSettings.logToConsole) {
559
- console.warn(message);
560
- }
561
- }
562
- // #endregion Warning handling.
563
- // #region infringement handling.
564
- /** Stores the settings concerning infringements */
565
- public infringementSettings: {
566
- throwException: boolean;
567
- logToConsole: boolean;
568
- } = { throwException: true, logToConsole: false };
569
- /** Sanitizes a value for safe inclusion in error messages. */
570
- private static sanitize(value: unknown): string {
571
- const str = typeof value === "string" ? value : String(value);
572
- return str.replace(/[<>&"']/g, (ch) => {
573
- switch (ch) {
574
- case "<":
575
- return "&lt;";
576
- case ">":
577
- return "&gt;";
578
- case "&":
579
- return "&amp;";
580
- case '"':
581
- return "&quot;";
582
- case "'":
583
- return "&#39;";
584
- default:
585
- return ch;
586
- }
587
- });
588
- }
589
- /**
590
- * Reports an infringement according to the {@link infringementSettings } also generating a proper {@link string }-wrapper
591
- * for the given "message" & violator.
592
- *
593
- * @param message The {@link string } describing the infringement and it's provenience.
594
- * @param violator The {@link string } describing or naming the violator. */
595
- protected reportInfringement(
596
- message: string,
597
- violator: string,
598
- target: object,
599
- value: unknown,
600
- path: string | undefined,
601
- hint: string | undefined = undefined,
602
- ): undefined {
603
- const safeViolator = DBC.sanitize(violator);
604
- const targetName =
605
- typeof target === "function"
606
- ? DBC.sanitize(target.name)
607
- : typeof target === "object" &&
608
- target !== null &&
609
- typeof target.constructor === "function"
610
- ? DBC.sanitize(target.constructor.name)
611
- : DBC.sanitize(target);
612
- const finalMessage: string = `[ From "${safeViolator}" in "${targetName}"${path ? ` > "${DBC.sanitize(path)}"` : ""}: ${message} ${hint ? `✨ ${hint} ✨` : ""}]`;
613
-
614
- if (this.infringementSettings.throwException) {
615
- throw new DBC.Infringement(finalMessage);
616
- }
617
-
618
- if (this.infringementSettings.logToConsole) {
619
- console.log(finalMessage);
620
- }
621
- }
622
- /**
623
- * Reports a parameter-infringement via {@link reportInfringement } also generating a proper {@link string }-wrapper
624
- * for the given "message","method", parameter-"index" & value.
625
- *
626
- * @param message The {@link string } describing the infringement and it's provenience.
627
- * @param method The {@link string } describing or naming the violator.
628
- * @param index The index of the parameter within the argument listing.
629
- * @param value The parameter's value. */
630
- public reportParameterInfringement(
631
- message: string,
632
- target: object,
633
- path: string | undefined,
634
- method: string,
635
- index: number,
636
- value: unknown,
637
- hint: string | undefined = undefined,
638
- ): undefined {
639
- const properIndex = index + 1;
640
-
641
- this.reportInfringement(
642
- `[ Parameter-value "${value}" of the ${properIndex}${properIndex === 1 ? "st" : properIndex === 2 ? "nd" : properIndex === 3 ? "rd" : "th"} parameter did not fulfill one of it's contracts: ${message} ]`,
643
- method,
644
- target,
645
- value,
646
- path,
647
- hint,
648
- );
649
- }
650
- /**
651
- * Reports a field-infringement via {@link reportInfringement } also generating a proper {@link string }-wrapper
652
- * for the given **message** & **name**.
653
- *
654
- * @param message A {@link string } describing the infringement and it's provenience.
655
- * @param key The property key.
656
- * @param path The dotted-path {@link string } that leads to the value not fulfilling the contract starting from
657
- * the tagged one.
658
- * @param value The value not fulfilling a contract. */
659
- public reportFieldInfringement(
660
- message: string,
661
- target: object,
662
- path: string | undefined,
663
- key: string,
664
- value: unknown,
665
- hint: string | undefined = undefined,
666
- ): undefined {
667
- this.reportInfringement(
668
- `[ New value for "${key}"${path === undefined ? "" : `.${path}`} with value "${value}" did not fulfill one of it's contracts: ${message} ]`,
669
- key,
670
- target,
671
- value,
672
- path,
673
- );
674
- }
675
- /**
676
- * Reports a returnvalue-infringement according via {@link reportInfringement } also generating a proper {@link string }-wrapper
677
- * for the given "message","method" & value.
678
- *
679
- * @param message The {@link string } describing the infringement and it's provenience.
680
- * @param method The {@link string } describing or naming the violator.
681
- * @param value The parameter's value. */
682
- public reportReturnvalueInfringement(
683
- message: string,
684
- target: object,
685
- path: string | undefined,
686
- method: string,
687
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
688
- value: any,
689
- hint: string | undefined = undefined,
690
- ) {
691
- this.reportInfringement(
692
- `[ Return-value "${value}" did not fulfill one of it's contracts: ${message} ]`,
693
- method,
694
- target,
695
- value,
696
- path,
697
- hint,
698
- );
699
- }
700
- /**
701
- * Routes an imperative infringement (from {@link tsCheck } or static {@link check } calls) through the
702
- * registered DBC instance's {@link infringementSettings }, respecting whether to throw, log, or both.
703
- * Falls back to throwing {@link DBC.Infringement } directly if no DBC instance is registered at the
704
- * specified path, preserving the pre-existing behaviour for code that does not register a DBC instance.
705
- *
706
- * @param message The fully formatted infringement message.
707
- * @param dbc The path to the DBC instance. Defaults to `"WaXCode.DBC"`. */
708
- public static reportTsCheckInfringement(
709
- message: string,
710
- dbc: string | undefined = undefined,
711
- ): void {
712
- let dbcInstance: DBC | undefined;
713
- try {
714
- dbcInstance = DBC.getDBC(dbc);
715
- } catch {
716
- throw new DBC.Infringement(message);
717
- }
718
-
719
- if (dbcInstance.infringementSettings.throwException) {
720
- throw new DBC.Infringement(message);
721
- }
722
-
723
- if (dbcInstance.infringementSettings.logToConsole) {
724
- console.log(message);
725
- }
726
- }
727
- // #region Classes
728
- // #region Errors
729
- /** An {@link Error } to be thrown whenever an infringement is detected. */
730
- public static Infringement = class extends Error {
731
- /**
732
- * Constructs this {@link Error } by tagging the specified message-{@link string } as an XDBC-Infringement.
733
- *
734
- * @param message The {@link string } describing the infringement. */
735
- constructor(message: string) {
736
- super(`[ XDBC Infringement ${message}]`);
737
- }
738
- };
739
- // #endregion Errors
740
- // #endregion Classes
741
- // #endregion infringement handling.
742
- /**
743
- * Resolves the specified dotted {@link string }-path to a {@link DBC }.
744
- *
745
- * @param obj The {@link object } to start resolving from.
746
- * @param path The dotted {@link string }-path leading to the {@link DBC }.
747
- *
748
- * @returns The requested {@link DBC }.
749
- */
750
- // biome-ignore lint/suspicious/noExplicitAny: Must traverse arbitrary object graphs
751
- static resolveDBCPath = (obj: any, path: string): DBC =>
752
- path
753
- ?.split(".")
754
- // biome-ignore lint/suspicious/noExplicitAny: Must traverse arbitrary object graphs
755
- .reduce((accumulator: any, current: string) => accumulator[current], obj);
756
- /**
757
- * Constructs this {@link DBC } without mounting it on the global namespace.
758
- * Use {@link DBC.register } to make the instance available at a specific path on globalThis.
759
- *
760
- * @param infringementSettings See {@link DBC.infringementSettings }.
761
- * @param executionSettings See {@link DBC.executionSettings }. */
762
- constructor(
763
- infringementSettings: {
764
- throwException: boolean;
765
- logToConsole: boolean;
766
- } = { throwException: true, logToConsole: false },
767
- executionSettings: {
768
- checkPreconditions: boolean;
769
- checkPostconditions: boolean;
770
- checkInvariants: boolean;
771
- } = {
772
- checkPreconditions: true,
773
- checkPostconditions: true,
774
- checkInvariants: true,
775
- },
776
- ) {
777
- this.infringementSettings = infringementSettings;
778
- this.executionSettings = executionSettings;
779
- }
780
- /**
781
- * Registers a {@link DBC } instance at the specified dotted path on globalThis (or window),
782
- * making it available for decorator resolution via string paths.
783
- *
784
- * @param instance The {@link DBC } instance to register.
785
- * @param path The dotted path to register at (default: `"WaXCode.DBC"`). */
786
- static register(instance: DBC, path = "WaXCode.DBC"): void {
787
- const segments = path.split(".");
788
- // biome-ignore lint/suspicious/noExplicitAny: Must walk dynamic global namespace.
789
- let obj: any = DBC.getHost();
790
- for (let i = 0; i < segments.length - 1; i++) {
791
- if (obj[segments[i]] === undefined) obj[segments[i]] = {};
792
- obj = obj[segments[i]];
793
- }
794
- obj[segments[segments.length - 1]] = instance;
795
- DBC.dbcCache.set(path, instance);
796
- }
797
- /**
798
- * Executes a callback with an isolated {@link DBC } instance temporarily registered at the default path.
799
- * The previous instance (if any) is restored after the callback completes — even if it throws.
800
- * Useful for test isolation.
801
- *
802
- * @param fn The callback receiving the isolated {@link DBC } instance. */
803
- static isolated(fn: (dbc: DBC) => void): void {
804
- const saved = DBC.dbcCache.get("WaXCode.DBC");
805
- const testDbc = new DBC();
806
- DBC.register(testDbc);
807
- try {
808
- fn(testDbc);
809
- } finally {
810
- if (saved) {
811
- DBC.register(saved);
812
- } else {
813
- DBC.dbcCache.delete("WaXCode.DBC");
814
- }
815
- }
816
- }
817
- /**
818
- * Resolves the desired {@link object } out a given one **toResolveFrom** using the specified **path**.
819
- *
820
- * @param toResolveFrom The {@link object } starting to resolve from.
821
- * @param path The dotted path-{@link string }.
822
- * This string uses ., [...], and () to represent accessing nested properties,
823
- * array elements/object keys, and calling methods, respectively, mimicking JavaScript syntax to navigate
824
- * an object's structure. Code, e.g. something like a.b( 1 as number ).c, will not be executed and
825
- * thus make the retrieval fail.
826
- *
827
- * @returns The requested {@link object }, NULL or UNDEFINED. */
828
- public static resolve(toResolveFrom: unknown, path: string) {
829
- if (!toResolveFrom || typeof path !== "string") {
830
- return undefined;
831
- }
832
-
833
- // Security: block prototype pollution paths
834
- const dangerousTokens = ["__proto__", "constructor", "prototype"];
835
-
836
- const cachedParts = DBC.pathTokenCache.get(path);
837
- const parts =
838
- cachedParts ?? path.replace(/\[(['"]?)(.*?)\1\]/g, ".$2").split(".");
839
-
840
- if (!cachedParts) {
841
- // Validate tokens before caching
842
- for (const part of parts) {
843
- const tokenName = part.replace(/\(.*\)$/, "");
844
- if (dangerousTokens.indexOf(tokenName) >= 0) {
845
- throw new Error(
846
- `[XDBC] Path "${path}" contains forbidden token "${tokenName}".`,
847
- );
848
- }
849
- }
850
- DBC.evictIfNeeded(DBC.pathTokenCache);
851
- DBC.pathTokenCache.set(path, parts);
852
- }
853
-
854
- // biome-ignore lint/suspicious/noExplicitAny: Must traverse arbitrary object graphs
855
- let current: any = toResolveFrom;
856
-
857
- for (const part of parts) {
858
- if (current === null || typeof current === "undefined") {
859
- return undefined;
860
- }
861
-
862
- const methodMatch = part.match(/(\w+)\((.*)\)/);
863
-
864
- if (methodMatch) {
865
- const methodName = methodMatch[1];
866
- const argsStr = methodMatch[2];
867
- const args = argsStr.split(",").map((arg) => arg.trim());
868
-
869
- if (typeof current[methodName] === "function") {
870
- current = current[methodName].apply(current, args);
871
- } else {
872
- return undefined;
873
- }
874
- } else {
875
- if (
876
- typeof window !== "undefined" &&
877
- typeof HTMLElement !== "undefined" &&
878
- current instanceof HTMLElement &&
879
- part.startsWith("@")
880
- ) {
881
- current = current.getAttribute(part.slice(1));
882
- } else if (
883
- typeof current === "object" &&
884
- current !== null &&
885
- part in current
886
- ) {
887
- current = current[part];
888
- } else if (
889
- typeof window !== "undefined" &&
890
- typeof HTMLElement !== "undefined" &&
891
- current instanceof HTMLElement
892
- ) {
893
- current = undefined;
894
- } else {
895
- current = undefined;
896
- }
897
- }
898
- }
899
-
900
- return current;
901
- }
902
- }
903
- // Register the default instance with standard settings.
904
- DBC.register(new DBC());
1
+ /**
2
+ * Provides a **D**esign **B**y **C**ontract Framework using decorators.
3
+ *
4
+ * @remarks
5
+ * Maintainer: Callari, Salvatore (XDBC@WaXCode.net) */
6
+ export class DBC {
7
+ // #region Internal caches.
8
+ private static readonly MAX_CACHE_SIZE = 1000;
9
+ private static dbcCache: Map<string, DBC> = new Map();
10
+ private static pathTokenCache: Map<string, string[]> = new Map();
11
+ /** Evicts the oldest entry if the cache exceeds the maximum size. */
12
+ private static evictIfNeeded<K, V>(cache: Map<K, V>): void {
13
+ if (cache.size > DBC.MAX_CACHE_SIZE) {
14
+ const oldest = cache.keys().next().value;
15
+ if (oldest !== undefined) cache.delete(oldest);
16
+ }
17
+ }
18
+ private static getHost(): unknown {
19
+ return typeof window !== "undefined" ? window : globalThis;
20
+ }
21
+ private static getDBC(dbc: string | DBC | undefined): DBC {
22
+ if (dbc instanceof DBC) return dbc;
23
+ const path = dbc ?? "WaXCode.DBC";
24
+ if (DBC.dbcCache.has(path)) {
25
+ return DBC.dbcCache.get(path) as DBC;
26
+ }
27
+ const resolved = DBC.resolveDBCPath(DBC.getHost(), path);
28
+ if (resolved) {
29
+ DBC.evictIfNeeded(DBC.dbcCache);
30
+ DBC.dbcCache.set(path, resolved);
31
+ return resolved;
32
+ }
33
+ throw new Error(
34
+ `[XDBC] DBC instance not found at path "${path}". Ensure a DBC instance is registered there.`,
35
+ );
36
+ }
37
+ /**
38
+ * Returns the registered {@link DBC } instance at the specified path.
39
+ * Throws if no instance is registered there.
40
+ *
41
+ * @param path The dotted path to look up (default: `"WaXCode.DBC"`). */
42
+ public static getRegistered(path?: string): DBC {
43
+ return DBC.getDBC(path);
44
+ }
45
+ // #endregion Internal caches.
46
+ // #region Parameter-value requests.
47
+ /** Stores all request for parameter values registered by {@link decPrecondition }. */
48
+ static paramValueRequests: Map<
49
+ string,
50
+ Map<number, Array<(value: any) => undefined>>
51
+ > = new Map<string, Map<number, Array<(value: any) => undefined>>>();
52
+ /**
53
+ * Generate a unique key for storing parameter value requests.
54
+ * Format: "ClassName:methodName"
55
+ */
56
+ private static getRequestKey(
57
+ target: object,
58
+ methodName: string | symbol | undefined,
59
+ ): string {
60
+ const className =
61
+ typeof target === "function"
62
+ ? target.name
63
+ : target.constructor?.name || "Unknown";
64
+ return `${className}:${String(methodName)}`;
65
+ }
66
+ /**
67
+ * Make a request to get the value of a certain parameter of specific method in a specific {@link object }.
68
+ * That request gets enlisted in {@link paramValueRequests } which is used by {@link ParamvalueProvider} to invoke the
69
+ * given "receptor" with the parameter value stored in there. Thus a parameter decorator using this method will
70
+ * not receive any value of the top method is not tagged with {@link ParamvalueProvider}.
71
+ *
72
+ * @param target The {@link object } containing the method with the parameter which's value is requested.
73
+ * @param methodName The name of the method with the parameter which's value is requested.
74
+ * @param index The index of the parameter which's value is requested.
75
+ * @param receptor The method the requested parameter-value shall be passed to when it becomes available. */
76
+ protected static requestParamValue(
77
+ target: object,
78
+ methodName: string | symbol | undefined,
79
+ index: number,
80
+ receptor: (value: any) => undefined,
81
+ ): undefined {
82
+ const key = DBC.getRequestKey(target, methodName);
83
+ if (DBC.paramValueRequests.has(key)) {
84
+ const paramMap = DBC.paramValueRequests.get(key)!;
85
+ if (paramMap.has(index)) {
86
+ paramMap.get(index)?.push(receptor);
87
+ } else {
88
+ paramMap.set(index, new Array<(value: unknown) => undefined>(receptor));
89
+ }
90
+ } else {
91
+ DBC.paramValueRequests.set(
92
+ key,
93
+ new Map<number, Array<(value: unknown) => undefined>>([
94
+ [index, new Array<(value: unknown) => undefined>(receptor)],
95
+ ]),
96
+ );
97
+ }
98
+ return undefined;
99
+ }
100
+ /**
101
+ * A decorator usable on both **methods** (including setters) and **classes**.
102
+ *
103
+ * - **On a method**: wraps the method so that any {@link DBC } preconditions registered on its parameters
104
+ * via XDBC parameter-decorator factories are dispatched with the actual argument values on each call.
105
+ * - **On a class**: wraps the constructor so that preconditions registered on constructor parameters are
106
+ * dispatched **before** the original constructor body runs, upholding true precondition semantics.
107
+ *
108
+ * In both cases the "receptor" callbacks enlisted in {@link paramValueRequests } by {@link requestParamValue }
109
+ * are invoked with the live argument values.
110
+ *
111
+ * @param targetOrConstructor When used as a **method** decorator: the {@link object } hosting the tagged method.
112
+ * When used as a **class** decorator: the class constructor.
113
+ * @param propertyKey *(method decorator only)* The tagged method's name as provided by the runtime.
114
+ * @param descriptor *(method decorator only)* The {@link PropertyDescriptor } as provided by the runtime.
115
+ *
116
+ * @returns When used as a **method** decorator: the (modified) {@link PropertyDescriptor }.
117
+ * When used as a **class** decorator: a replacement constructor that performs precondition checks. */
118
+ public static ParamvalueProvider(
119
+ target: object,
120
+ propertyKey: string,
121
+ descriptor: PropertyDescriptor,
122
+ ): PropertyDescriptor;
123
+ public static ParamvalueProvider<T extends new (...args: any[]) => any>(
124
+ ctor: T,
125
+ ): T;
126
+ public static ParamvalueProvider<
127
+ T extends abstract new (
128
+ ...args: any[]
129
+ ) => any,
130
+ >(ctor: T): T;
131
+ public static ParamvalueProvider(...args: any[]): any {
132
+ if (args.length === 1 && typeof args[0] === "function") {
133
+ // #region Class decorator path
134
+ const ctor = args[0] as new (...args: any[]) => any;
135
+ const key = `${ctor.name}:undefined`;
136
+ const WrappedClass = class extends ctor {
137
+ constructor(...ctorArgs: any[]) {
138
+ if (DBC.paramValueRequests.has(key)) {
139
+ const paramMap = DBC.paramValueRequests.get(key)!;
140
+ for (const index of paramMap.keys()) {
141
+ if (index < ctorArgs.length) {
142
+ for (const receptor of paramMap.get(index)!) {
143
+ receptor(ctorArgs[index]);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ super(...ctorArgs);
149
+ }
150
+ } as typeof ctor;
151
+ Object.defineProperty(WrappedClass, "name", { value: ctor.name });
152
+ return WrappedClass;
153
+ // #endregion Class decorator path
154
+ }
155
+ // #region Method decorator path
156
+ const [target, propertyKey, descriptor] = args as [
157
+ object,
158
+ string,
159
+ PropertyDescriptor,
160
+ ];
161
+ const originalMethod = descriptor.value;
162
+ const isStatic = typeof target === "function";
163
+ descriptor.value = function (...methodArgs: any[]) {
164
+ // #region Check if a value of one of the method's parameter has been requested and pass it to the
165
+ // receptor, if so.
166
+ const actualTarget = isStatic ? this : (this as any).constructor;
167
+ const key = DBC.getRequestKey(actualTarget, propertyKey);
168
+ if (DBC.paramValueRequests.has(key)) {
169
+ const paramMap = DBC.paramValueRequests.get(key)!;
170
+ for (const index of paramMap.keys()) {
171
+ if (index < methodArgs.length) {
172
+ for (const receptor of paramMap.get(index)!) {
173
+ receptor(methodArgs[index]);
174
+ }
175
+ }
176
+ }
177
+ } else {
178
+ console.warn("No parameter value requests found for key:", key);
179
+ }
180
+ // #endregion Check if a value of one of the method's parameter has been requested and pass it to the
181
+ // receptor, if so.
182
+ return originalMethod.apply(this, methodArgs);
183
+ };
184
+ return descriptor;
185
+ // #endregion Method decorator path
186
+ }
187
+ // #endregion Parameter-value requests.
188
+ // #region Class
189
+ /**
190
+ * A property-decorator factory serving as a **D**esign **B**y **C**ontract Invariant.
191
+ * This invariant aims to check the instance of the class not the value to be get or set.
192
+ *
193
+ * @param contracts The {@link DBC }-Contracts the value shall uphold.
194
+ *
195
+ * @throws A {@link DBC.Infringement } whenever the property is tried to be get or set without the instance of it's class
196
+ * fulfilling the specified **contracts**. */
197
+ public static decClassInvariant(
198
+ contracts: Array<{
199
+ check: (toCheck: unknown | null | undefined) => boolean | string;
200
+ }>,
201
+ path: string | undefined = undefined,
202
+ dbc = "WaXCode.DBC",
203
+ ) {
204
+ let dbcInstance: DBC | undefined;
205
+ return (
206
+ target: unknown,
207
+ propertyKey: string | symbol,
208
+ descriptor: PropertyDescriptor,
209
+ ) => {
210
+ if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
211
+ if (!dbcInstance.executionSettings.checkInvariants) {
212
+ return;
213
+ }
214
+ const originalSetter = descriptor.set;
215
+ const originalGetter = descriptor.get;
216
+ let value: any;
217
+ // #region Replace original property.
218
+ Object.defineProperty(target, propertyKey, {
219
+ get() {
220
+ if (!dbcInstance?.executionSettings.checkInvariants) {
221
+ return;
222
+ }
223
+ const realValue = path ? DBC.resolve(this, path) : this;
224
+ // #region Check if all "contracts" are fulfilled.
225
+ for (const contract of contracts) {
226
+ const result = contract.check(realValue);
227
+ if (typeof result === "string") {
228
+ dbcInstance?.reportFieldInfringement(
229
+ result,
230
+ target as object,
231
+ path,
232
+ propertyKey as string,
233
+ realValue,
234
+ );
235
+ }
236
+ }
237
+ // #endregion Check if all "contracts" are fulfilled.
238
+ return originalGetter ? (originalGetter as any)[propertyKey] : value;
239
+ },
240
+ set(newValue) {
241
+ if (!dbcInstance?.executionSettings.checkInvariants) {
242
+ return;
243
+ }
244
+ const realValue = path ? DBC.resolve(this, path) : this;
245
+ // #region Check if all "contracts" are fulfilled.
246
+ for (const contract of contracts) {
247
+ const result = contract.check(realValue);
248
+ if (typeof result === "string") {
249
+ dbcInstance?.reportFieldInfringement(
250
+ result,
251
+ target as object,
252
+ path,
253
+ propertyKey as string,
254
+ realValue,
255
+ );
256
+ }
257
+ }
258
+ // #endregion Check if all "contracts" are fulfilled.
259
+ value = newValue;
260
+ },
261
+ enumerable: true,
262
+ configurable: true,
263
+ });
264
+ // #endregion Replace original property.
265
+ };
266
+ }
267
+ // #endregion Class
268
+ // #region Invariant
269
+ /**
270
+ * A property-decorator factory serving as a **D**esign **B**y **C**ontract Invariant.
271
+ * Since the value must be initialized or set according to the specified **contracts** the value will only be checked
272
+ * when assigning it.
273
+ *
274
+ * @param contracts The {@link DBC }-Contracts the value shall uphold.
275
+ *
276
+ * @throws A {@link DBC.Infringement } whenever the property is tried to be set to a value that does not comply to the
277
+ * specified **contracts**, by the returned method.*/
278
+ public static decInvariant(
279
+ contracts: Array<{
280
+ check: (toCheck: unknown | null | undefined) => boolean | string;
281
+ }>,
282
+ path: string | undefined = undefined,
283
+ dbc: string | undefined = undefined,
284
+ hint: string | undefined = undefined,
285
+ ) {
286
+ let dbcInstance: DBC | undefined;
287
+ return (target: unknown, propertyKey: string | symbol) => {
288
+ if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
289
+ if (!dbcInstance.executionSettings.checkInvariants) {
290
+ return;
291
+ }
292
+ let value: any;
293
+ // #region Replace original property.
294
+ Object.defineProperty(target, propertyKey, {
295
+ set(newValue) {
296
+ if (!dbcInstance?.executionSettings.checkInvariants) {
297
+ return;
298
+ }
299
+ const realValue = path ? DBC.resolve(newValue, path) : newValue;
300
+ // #region Check if all "contracts" are fulfilled.
301
+ for (const contract of contracts) {
302
+ const result = contract.check(realValue);
303
+ if (typeof result === "string") {
304
+ dbcInstance?.reportFieldInfringement(
305
+ result,
306
+ target as object,
307
+ path,
308
+ propertyKey as string,
309
+ realValue,
310
+ hint,
311
+ );
312
+ }
313
+ }
314
+ // #endregion Check if all "contracts" are fulfilled.
315
+ value = newValue;
316
+ },
317
+ enumerable: true,
318
+ configurable: true,
319
+ });
320
+ // #endregion Replace original property.
321
+ };
322
+ }
323
+ // #endregion Invariant
324
+ // #region Postcondition
325
+ /**
326
+ * A method decorator factory checking the result of a method whenever it is invoked thus also usable on getters.
327
+ *
328
+ * @param check The **(toCheck: any, object, string) => boolean | string** to use for checking.
329
+ * @param dbc See {@link DBC.resolveDBCPath }.
330
+ * @param path The dotted path referring to the actual value to check, starting form the specified one.
331
+ *
332
+ * @returns The **( target : object, propertyKey : string, descriptor : PropertyDescriptor ) : PropertyDescriptor**
333
+ * invoked by Typescript.
334
+ */
335
+ public static decPostcondition(
336
+ check: (
337
+ toCheck: any,
338
+ target: object,
339
+ propertyKey: string,
340
+ ) => boolean | string,
341
+ dbc: string | undefined = undefined,
342
+ path: string | undefined = undefined,
343
+ hint: string | undefined = undefined,
344
+ ) {
345
+ let dbcInstance: DBC | undefined;
346
+ return (
347
+ target: object,
348
+ propertyKey: string,
349
+ descriptor: PropertyDescriptor,
350
+ ): PropertyDescriptor => {
351
+ const originalMethod = descriptor.value;
352
+ descriptor.value = (...args: any[]) => {
353
+ if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
354
+ if (!dbcInstance.executionSettings.checkPostconditions) {
355
+ return;
356
+ }
357
+ // biome-ignore lint/complexity/noThisInStatic: <explanation>
358
+ const result = originalMethod.apply(this, args);
359
+ const realValue = path ? DBC.resolve(result, path) : result;
360
+ const checkResult = check(realValue, target, propertyKey);
361
+ if (typeof checkResult === "string") {
362
+ dbcInstance.reportReturnvalueInfringement(
363
+ checkResult,
364
+ target,
365
+ path,
366
+ propertyKey,
367
+ realValue,
368
+ hint,
369
+ );
370
+ }
371
+ return result;
372
+ };
373
+ return descriptor;
374
+ };
375
+ }
376
+ // #endregion Postcondition
377
+ // #region Decorator
378
+ // #region Precondition
379
+ /**
380
+ * A parameter-decorator factory that requests the tagged parameter's value passing it to the provided
381
+ * "check"-method when the value becomes available.
382
+ *
383
+ * @param check The "( unknown ) => void" to be invoked along with the tagged parameter's value as soon
384
+ * as it becomes available.
385
+ * @param dbc See {@link DBC.resolveDBCPath }.
386
+ * @param path The dotted path referring to the actual value to check, starting form the specified one.
387
+ * May contain :: to separate multiple paths.
388
+ *
389
+ * @returns The **(target: object, methodName: string | symbol, parameterIndex: number ) => void** invoked by Typescript- */
390
+ protected static decPrecondition(
391
+ check: (
392
+ value: unknown,
393
+ target: object,
394
+ methodName: string | symbol | undefined,
395
+ parameterIndex: number,
396
+ ) => boolean | string,
397
+ dbc: string | undefined = undefined,
398
+ path: string | undefined = undefined,
399
+ hint: string | undefined = undefined,
400
+ ): (
401
+ target: object,
402
+ methodName: string | symbol | undefined,
403
+ parameterIndex: number,
404
+ ) => void {
405
+ const paths = path ? path.replace(/ /g, "").split("::") : [undefined];
406
+ let dbcInstance: DBC | undefined;
407
+ return (
408
+ target: object,
409
+ methodName: string | symbol | undefined,
410
+ parameterIndex: number,
411
+ ): void => {
412
+ DBC.requestParamValue(
413
+ target,
414
+ methodName,
415
+ parameterIndex,
416
+ (value: unknown) => {
417
+ if (!dbcInstance) dbcInstance = DBC.getDBC(dbc);
418
+ if (!dbcInstance.executionSettings.checkPreconditions) {
419
+ return;
420
+ }
421
+ for (const singlePath of paths) {
422
+ const realValue = singlePath
423
+ ? DBC.resolve(value, singlePath)
424
+ : value;
425
+ const result = check(realValue, target, methodName, parameterIndex);
426
+ if (typeof result === "string") {
427
+ dbcInstance.reportParameterInfringement(
428
+ result,
429
+ target,
430
+ singlePath,
431
+ methodName as string,
432
+ parameterIndex,
433
+ realValue,
434
+ hint,
435
+ );
436
+ }
437
+ }
438
+ },
439
+ );
440
+ };
441
+ }
442
+ // #endregion Precondition
443
+ // #endregion Decorator
444
+ // #region Contract Factory Helpers
445
+ /**
446
+ * Creates a PRE decorator from a checkAlgorithm function and its bound arguments.
447
+ * Reduces boilerplate across contract classes.
448
+ *
449
+ * @param checkFn A function that takes (value, ...boundArgs) and returns true or an error string.
450
+ * @param boundArgs The arguments to bind to the check function after the value.
451
+ * @param dbc See {@link DBC.decPrecondition}.
452
+ * @param path See {@link DBC.decPrecondition}.
453
+ * @param hint See {@link DBC.decPrecondition}.
454
+ */
455
+ public static createPRE(
456
+ checkFn: (...args: any[]) => boolean | string,
457
+ boundArgs: any[],
458
+ dbc?: string,
459
+ path?: string,
460
+ hint?: string,
461
+ ) {
462
+ return DBC.decPrecondition(
463
+ (value, _target, _methodName, _parameterIndex) => {
464
+ return checkFn(value, ...boundArgs);
465
+ },
466
+ dbc,
467
+ path,
468
+ hint,
469
+ );
470
+ }
471
+ /**
472
+ * Creates a POST decorator from a checkAlgorithm function and its bound arguments.
473
+ *
474
+ * @param checkFn A function that takes (value, ...boundArgs) and returns true or an error string.
475
+ * @param boundArgs The arguments to bind to the check function after the value.
476
+ * @param dbc See {@link DBC.decPostcondition}.
477
+ * @param path See {@link DBC.decPostcondition}.
478
+ * @param hint See {@link DBC.decPostcondition}.
479
+ */
480
+ public static createPOST(
481
+ checkFn: (...args: any[]) => boolean | string,
482
+ boundArgs: any[],
483
+ dbc?: string,
484
+ path?: string,
485
+ hint?: string,
486
+ ) {
487
+ return DBC.decPostcondition(
488
+ (value, _target, _propertyKey) => {
489
+ return checkFn(value, ...boundArgs);
490
+ },
491
+ dbc,
492
+ path,
493
+ hint,
494
+ );
495
+ }
496
+ /**
497
+ * Creates an INVARIANT decorator from a contract constructor and its bound arguments.
498
+ *
499
+ * @param ContractClass A class with a constructor that produces an object with a `check` method.
500
+ * @param ctorArgs The arguments to pass to the contract constructor.
501
+ * @param dbc See {@link DBC.decInvariant}.
502
+ * @param path See {@link DBC.decInvariant}.
503
+ * @param hint See {@link DBC.decInvariant}.
504
+ */
505
+ public static createINVARIANT(
506
+ ContractClass: new (
507
+ ...args: any[]
508
+ ) => { check: (toCheck: unknown | null | undefined) => boolean | string },
509
+ ctorArgs: any[],
510
+ dbc?: string,
511
+ path?: string,
512
+ hint?: string,
513
+ ) {
514
+ return DBC.decInvariant([new ContractClass(...ctorArgs)], path, dbc, hint);
515
+ }
516
+ // #endregion Contract Factory Helpers
517
+ // #region Execution Handling
518
+ /** Stores settings concerning the execution of checks. */
519
+ public executionSettings: {
520
+ checkPreconditions: boolean;
521
+ checkPostconditions: boolean;
522
+ checkInvariants: boolean;
523
+ } = {
524
+ checkPreconditions: true,
525
+ checkPostconditions: true,
526
+ checkInvariants: true,
527
+ };
528
+ // #endregion Execution Handling
529
+ // #region Warning handling.
530
+ /** Stores settings concerning warnings. */
531
+ public warningSettings: {
532
+ logToConsole: boolean;
533
+ } = { logToConsole: true };
534
+ /**
535
+ * Reports a warning.
536
+ *
537
+ * @param message The message containing the warning. */
538
+ protected reportWarning(message: string): undefined {
539
+ if (this.warningSettings.logToConsole) {
540
+ console.warn(message);
541
+ }
542
+ }
543
+ // #endregion Warning handling.
544
+ // #region infringement handling.
545
+ /** Stores the settings concerning infringements */
546
+ public infringementSettings: {
547
+ throwException: boolean;
548
+ logToConsole: boolean;
549
+ onInfringement:
550
+ | ((
551
+ infringement: InstanceType<typeof DBC.Infringement>,
552
+ context: {
553
+ type: "precondition" | "postcondition" | "invariant";
554
+ value: unknown;
555
+ },
556
+ ) => void)
557
+ | undefined;
558
+ } = { throwException: true, logToConsole: false, onInfringement: undefined };
559
+ /** Sanitizes a value for safe inclusion in error messages. */
560
+ private static sanitize(value: unknown): string {
561
+ const str = typeof value === "string" ? value : String(value);
562
+ return str.replace(/[<>&"']/g, (ch) => {
563
+ switch (ch) {
564
+ case "<":
565
+ return "&lt;";
566
+ case ">":
567
+ return "&gt;";
568
+ case "&":
569
+ return "&amp;";
570
+ case '"':
571
+ return "&quot;";
572
+ case "'":
573
+ return "&#39;";
574
+ default:
575
+ return ch;
576
+ }
577
+ });
578
+ }
579
+ /**
580
+ * Reports an infringement according to the {@link infringementSettings } also generating a proper {@link string }-wrapper
581
+ * for the given "message" & violator.
582
+ *
583
+ * @param message The {@link string } describing the infringement and it's provenience.
584
+ * @param violator The {@link string } describing or naming the violator. */
585
+ protected reportInfringement(
586
+ message: string,
587
+ violator: string,
588
+ target: object,
589
+ value: unknown,
590
+ path: string | undefined,
591
+ hint: string | undefined = undefined,
592
+ type: "precondition" | "postcondition" | "invariant" = "precondition",
593
+ ): undefined {
594
+ const safeViolator = DBC.sanitize(violator);
595
+ const targetName =
596
+ typeof target === "function"
597
+ ? DBC.sanitize(target.name)
598
+ : typeof target === "object" &&
599
+ target !== null &&
600
+ typeof target.constructor === "function"
601
+ ? DBC.sanitize(target.constructor.name)
602
+ : DBC.sanitize(target);
603
+ const finalMessage: string = `[ From "${safeViolator}" in "${targetName}"${path ? ` > "${DBC.sanitize(path)}"` : ""}: ${message} ${hint ? `✨ ${hint} ✨` : ""}]`;
604
+ const infringement = new DBC.Infringement(finalMessage);
605
+ if (this.infringementSettings.logToConsole) {
606
+ console.log(finalMessage);
607
+ }
608
+ if (this.infringementSettings.onInfringement) {
609
+ this.infringementSettings.onInfringement(infringement, { type, value });
610
+ }
611
+ if (this.infringementSettings.throwException) {
612
+ throw infringement;
613
+ }
614
+ }
615
+ /**
616
+ * Reports a parameter-infringement via {@link reportInfringement } also generating a proper {@link string }-wrapper
617
+ * for the given "message","method", parameter-"index" & value.
618
+ *
619
+ * @param message The {@link string } describing the infringement and it's provenience.
620
+ * @param method The {@link string } describing or naming the violator.
621
+ * @param index The index of the parameter within the argument listing.
622
+ * @param value The parameter's value. */
623
+ public reportParameterInfringement(
624
+ message: string,
625
+ target: object,
626
+ path: string | undefined,
627
+ method: string,
628
+ index: number,
629
+ value: unknown,
630
+ hint: string | undefined = undefined,
631
+ ): undefined {
632
+ const properIndex = index + 1;
633
+ this.reportInfringement(
634
+ `[ Parameter-value "${value}" of the ${properIndex}${properIndex === 1 ? "st" : properIndex === 2 ? "nd" : properIndex === 3 ? "rd" : "th"} parameter did not fulfill one of it's contracts: ${message} ]`,
635
+ method,
636
+ target,
637
+ value,
638
+ path,
639
+ hint,
640
+ "precondition",
641
+ );
642
+ }
643
+ /**
644
+ * Reports a field-infringement via {@link reportInfringement } also generating a proper {@link string }-wrapper
645
+ * for the given **message** & **name**.
646
+ *
647
+ * @param message A {@link string } describing the infringement and it's provenience.
648
+ * @param key The property key.
649
+ * @param path The dotted-path {@link string } that leads to the value not fulfilling the contract starting from
650
+ * the tagged one.
651
+ * @param value The value not fulfilling a contract. */
652
+ public reportFieldInfringement(
653
+ message: string,
654
+ target: object,
655
+ path: string | undefined,
656
+ key: string,
657
+ value: unknown,
658
+ hint: string | undefined = undefined,
659
+ ): undefined {
660
+ this.reportInfringement(
661
+ `[ New value for "${key}"${path === undefined ? "" : `.${path}`} with value "${value}" did not fulfill one of it's contracts: ${message} ]`,
662
+ key,
663
+ target,
664
+ value,
665
+ path,
666
+ hint,
667
+ "invariant",
668
+ );
669
+ }
670
+ /**
671
+ * Reports a returnvalue-infringement according via {@link reportInfringement } also generating a proper {@link string }-wrapper
672
+ * for the given "message","method" & value.
673
+ *
674
+ * @param message The {@link string } describing the infringement and it's provenience.
675
+ * @param method The {@link string } describing or naming the violator.
676
+ * @param value The parameter's value. */
677
+ public reportReturnvalueInfringement(
678
+ message: string,
679
+ target: object,
680
+ path: string | undefined,
681
+ method: string,
682
+ value: any,
683
+ hint: string | undefined = undefined,
684
+ ) {
685
+ this.reportInfringement(
686
+ `[ Return-value "${value}" did not fulfill one of it's contracts: ${message} ]`,
687
+ method,
688
+ target,
689
+ value,
690
+ path,
691
+ hint,
692
+ "postcondition",
693
+ );
694
+ }
695
+ /**
696
+ * Routes an imperative infringement (from {@link tsCheck } or static {@link check } calls) through the
697
+ * registered DBC instance's {@link infringementSettings }, respecting whether to throw, log, or both.
698
+ * Falls back to throwing {@link DBC.Infringement } directly if no DBC instance is registered at the
699
+ * specified path, preserving the pre-existing behaviour for code that does not register a DBC instance.
700
+ *
701
+ * @param message The fully formatted infringement message.
702
+ * @param dbc The path to the DBC instance. Defaults to `"WaXCode.DBC"`. */
703
+ public static reportTsCheckInfringement(
704
+ message: string,
705
+ dbc: string | undefined = undefined,
706
+ value: unknown = undefined,
707
+ ): void {
708
+ const infringement = new DBC.Infringement(message);
709
+ let dbcInstance: DBC | undefined;
710
+ try {
711
+ dbcInstance = DBC.getDBC(dbc);
712
+ } catch {
713
+ throw infringement;
714
+ }
715
+ if (dbcInstance.infringementSettings.logToConsole) {
716
+ console.log(message);
717
+ }
718
+ if (dbcInstance.infringementSettings.onInfringement) {
719
+ dbcInstance.infringementSettings.onInfringement(infringement, {
720
+ type: "precondition",
721
+ value,
722
+ });
723
+ }
724
+ if (dbcInstance.infringementSettings.throwException) {
725
+ throw infringement;
726
+ }
727
+ }
728
+ // #region Classes
729
+ // #region Errors
730
+ /** An {@link Error } to be thrown whenever an infringement is detected. */
731
+ public static Infringement = class extends Error {
732
+ /**
733
+ * Constructs this {@link Error } by tagging the specified message-{@link string } as an XDBC-Infringement.
734
+ *
735
+ * @param message The {@link string } describing the infringement. */
736
+ constructor(message: string) {
737
+ super(`[ XDBC Infringement ${message}]`);
738
+ }
739
+ };
740
+ // #endregion Errors
741
+ // #endregion Classes
742
+ // #endregion infringement handling.
743
+ /**
744
+ * Resolves the specified dotted {@link string }-path to a {@link DBC }.
745
+ *
746
+ * @param obj The {@link object } to start resolving from.
747
+ * @param path The dotted {@link string }-path leading to the {@link DBC }.
748
+ *
749
+ * @returns The requested {@link DBC }.
750
+ */
751
+ static resolveDBCPath = (obj: any, path: string): DBC =>
752
+ path
753
+ ?.split(".")
754
+ .reduce((accumulator: any, current: string) => accumulator[current], obj);
755
+ /**
756
+ * Constructs this {@link DBC } without mounting it on the global namespace.
757
+ * Use {@link DBC.register } to make the instance available at a specific path on globalThis.
758
+ *
759
+ * @param infringementSettings See {@link DBC.infringementSettings }.
760
+ * @param executionSettings See {@link DBC.executionSettings }. */
761
+ constructor(
762
+ infringementSettings: {
763
+ throwException: boolean;
764
+ logToConsole: boolean;
765
+ onInfringement?: (
766
+ infringement: InstanceType<typeof DBC.Infringement>,
767
+ context: {
768
+ type: "precondition" | "postcondition" | "invariant";
769
+ value: unknown;
770
+ },
771
+ ) => void;
772
+ } = { throwException: true, logToConsole: false },
773
+ executionSettings: {
774
+ checkPreconditions: boolean;
775
+ checkPostconditions: boolean;
776
+ checkInvariants: boolean;
777
+ } = {
778
+ checkPreconditions: true,
779
+ checkPostconditions: true,
780
+ checkInvariants: true,
781
+ },
782
+ ) {
783
+ this.infringementSettings = {
784
+ ...infringementSettings,
785
+ onInfringement: infringementSettings.onInfringement ?? undefined,
786
+ };
787
+ this.executionSettings = executionSettings;
788
+ }
789
+ /**
790
+ * Registers a {@link DBC } instance at the specified dotted path on globalThis (or window),
791
+ * making it available for decorator resolution via string paths.
792
+ *
793
+ * @param instance The {@link DBC } instance to register.
794
+ * @param path The dotted path to register at (default: `"WaXCode.DBC"`). */
795
+ static register(instance: DBC, path = "WaXCode.DBC"): void {
796
+ const segments = path.split(".");
797
+ let obj: any = DBC.getHost();
798
+ for (let i = 0; i < segments.length - 1; i++) {
799
+ if (obj[segments[i]] === undefined) obj[segments[i]] = {};
800
+ obj = obj[segments[i]];
801
+ }
802
+ obj[segments[segments.length - 1]] = instance;
803
+ DBC.dbcCache.set(path, instance);
804
+ }
805
+ /**
806
+ * Executes a callback with an isolated {@link DBC } instance temporarily registered at the default path.
807
+ * The previous instance (if any) is restored after the callback completes — even if it throws.
808
+ * Useful for test isolation.
809
+ *
810
+ * @param fn The callback receiving the isolated {@link DBC } instance. */
811
+ static isolated(fn: (dbc: DBC) => void): void {
812
+ const saved = DBC.dbcCache.get("WaXCode.DBC");
813
+ const testDbc = new DBC();
814
+ DBC.register(testDbc);
815
+ try {
816
+ fn(testDbc);
817
+ } finally {
818
+ if (saved) {
819
+ DBC.register(saved);
820
+ } else {
821
+ DBC.dbcCache.delete("WaXCode.DBC");
822
+ }
823
+ }
824
+ }
825
+ /**
826
+ * Resolves the desired {@link object } out a given one **toResolveFrom** using the specified **path**.
827
+ *
828
+ * @param toResolveFrom The {@link object } starting to resolve from.
829
+ * @param path The dotted path-{@link string }.
830
+ * This string uses ., [...], and () to represent accessing nested properties,
831
+ * array elements/object keys, and calling methods, respectively, mimicking JavaScript syntax to navigate
832
+ * an object's structure. Code, e.g. something like a.b( 1 as number ).c, will not be executed and
833
+ * thus make the retrieval fail.
834
+ *
835
+ * @returns The requested {@link object }, NULL or UNDEFINED. */
836
+ public static resolve(toResolveFrom: unknown, path: string) {
837
+ if (!toResolveFrom || typeof path !== "string") {
838
+ return undefined;
839
+ }
840
+ // Security: block prototype pollution paths
841
+ const dangerousTokens = ["__proto__", "constructor", "prototype"];
842
+ const cachedParts = DBC.pathTokenCache.get(path);
843
+ const parts =
844
+ cachedParts ?? path.replace(/\[(['"]?)(.*?)\1\]/g, ".$2").split(".");
845
+ if (!cachedParts) {
846
+ // Validate tokens before caching
847
+ for (const part of parts) {
848
+ const tokenName = part.replace(/\(.*\)$/, "");
849
+ if (dangerousTokens.indexOf(tokenName) >= 0) {
850
+ throw new Error(
851
+ `[XDBC] Path "${path}" contains forbidden token "${tokenName}".`,
852
+ );
853
+ }
854
+ }
855
+ DBC.evictIfNeeded(DBC.pathTokenCache);
856
+ DBC.pathTokenCache.set(path, parts);
857
+ }
858
+ let current: any = toResolveFrom;
859
+ for (const part of parts) {
860
+ if (current === null || typeof current === "undefined") {
861
+ return undefined;
862
+ }
863
+ const methodMatch = part.match(/(\w+)\((.*)\)/);
864
+ if (methodMatch) {
865
+ const methodName = methodMatch[1];
866
+ const argsStr = methodMatch[2];
867
+ const args = argsStr.split(",").map((arg) => arg.trim());
868
+ if (typeof current[methodName] === "function") {
869
+ current = current[methodName].apply(current, args);
870
+ } else {
871
+ return undefined;
872
+ }
873
+ } else {
874
+ if (
875
+ typeof window !== "undefined" &&
876
+ typeof HTMLElement !== "undefined" &&
877
+ current instanceof HTMLElement &&
878
+ part.startsWith("@")
879
+ ) {
880
+ current = current.getAttribute(part.slice(1));
881
+ } else if (
882
+ typeof current === "object" &&
883
+ current !== null &&
884
+ part in current
885
+ ) {
886
+ current = current[part];
887
+ } else if (
888
+ typeof window !== "undefined" &&
889
+ typeof HTMLElement !== "undefined" &&
890
+ current instanceof HTMLElement
891
+ ) {
892
+ current = undefined;
893
+ } else {
894
+ current = undefined;
895
+ }
896
+ }
897
+ }
898
+ return current;
899
+ }
900
+ }
901
+ // Register the default instance with standard settings.
902
+ DBC.register(new DBC());