@controlium/utils 1.0.1 → 1.0.2-alpha.2

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.
@@ -31,9 +31,6 @@ const MockUtils = {
31
31
  * - **expression** — what to compute (type-specific)
32
32
  * - **format** — how to format the result (type-specific, often optional)
33
33
  *
34
- * The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and
35
- * {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases.
36
- *
37
34
  * ---
38
35
  * ## Built-in token types
39
36
  *
@@ -138,77 +135,21 @@ const MockUtils = {
138
135
  *
139
136
  * ---
140
137
  * ## Extending with callbacks
141
- * Custom token types are registered via {@link Detokeniser.addCallbackSync} (for sync processing) or
142
- * {@link Detokeniser.addCallbackAsync} (for async processing). Callbacks are tried in registration
143
- * order; return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
144
- * built-in handler matched, an error is thrown.
138
+ * Custom token types are registered via {@link Detokeniser.addCallback}. Both sync and async handlers
139
+ * share the same registration method and callback list. Callbacks are tried in registration order;
140
+ * return `undefined` to pass to the next callback. If all callbacks return `undefined` and no built-in
141
+ * handler matched, an error is thrown.
142
+ *
143
+ * Async callbacks are silently skipped when {@link Detokeniser.do} (sync) is used — use
144
+ * {@link Detokeniser.doAsync} if your callback returns a Promise.
145
145
  *
146
- * @see {@link Detokeniser.addCallbackSync}
147
- * @see {@link Detokeniser.addCallbackAsync}
146
+ * @see {@link Detokeniser.addCallback}
148
147
  * @see {@link Detokeniser.do}
149
148
  * @see {@link Detokeniser.doAsync}
150
149
  */
151
150
  export class Detokeniser {
152
- /**
153
- * Gets the current single-character delimiter used to separate token parts.
154
- * Default: `|`
155
- */
156
- static get delimiter() {
157
- return this._delimiter;
158
- }
159
- /**
160
- * Sets the single-character delimiter used to separate token parts.
161
- * The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls.
162
- * @param newDelimiter - Exactly one character
163
- * @throws If `newDelimiter` is not exactly one character
164
- * @example
165
- * Detokeniser.delimiter = ':';
166
- * Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910'
167
- * Detokeniser.reset(); // restore default '|'
168
- */
169
- static set delimiter(newDelimiter) {
170
- if (newDelimiter.length !== 1) {
171
- const errTxt = `Invalid delimiter [${newDelimiter}]. Must be exactly 1 character!`;
172
- Log.writeLine(LogLevels.Error, errTxt);
173
- throw new Error(errTxt);
174
- }
175
- else {
176
- this._delimiter = newDelimiter;
177
- }
178
- }
179
- /**
180
- * Sets the start and end token sequences.
181
- * @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence.
182
- * @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]"
183
- * @remarks Start and end sequences must differ. Minimum total length is 2 (one char each).
184
- */
185
- static set tokenStartEndChars(startEndChars) {
186
- if (startEndChars.length < 2 || startEndChars.length % 2 !== 0) {
187
- const errTxt = `Invalid start/end chars [${startEndChars}]. Must be an even number of characters (minimum 2) — first half as start sequence, second half as end sequence!`;
188
- Log.writeLine(LogLevels.Error, errTxt);
189
- throw new Error(errTxt);
190
- }
191
- const half = startEndChars.length / 2;
192
- const start = startEndChars.substring(0, half);
193
- const end = startEndChars.substring(half);
194
- if (start === end) {
195
- const errTxt = `Invalid start/end chars — start sequence [${start}] must differ from end sequence [${end}]!`;
196
- Log.writeLine(LogLevels.Error, errTxt);
197
- throw new Error(errTxt);
198
- }
199
- this._startTokenChar = start;
200
- this._endTokenChar = end;
201
- }
202
- /**
203
- * Gets the current token start/end sequences concatenated (e.g. `"[[]]"`).
204
- */
205
- static get tokenStartEndChars() {
206
- return this._startTokenChar + this._endTokenChar;
207
- }
208
151
  /**
209
152
  * Resets the Detokeniser to factory defaults:
210
- * - Token endstops restored to `[[` / `]]`
211
- * - Delimiter restored to `|`
212
153
  * - Escape char restored to `/`
213
154
  * - All registered sync and async callbacks cleared
214
155
  *
@@ -217,121 +158,58 @@ export class Detokeniser {
217
158
  * afterEach(() => Detokeniser.reset());
218
159
  */
219
160
  static reset() {
220
- this._endTokenChar = "]]";
221
- this._startTokenChar = "[[";
222
161
  this.EscapeChar = "/";
223
- this._delimiter = "|";
224
- if (this._syncCallbacks) {
225
- this._syncCallbacks = [];
226
- }
227
- if (this._asyncCallbacks) {
228
- this._asyncCallbacks = [];
229
- }
230
- this._asyncCallbacks = undefined;
231
- this._syncCallbacks = undefined;
162
+ this._callbacks = undefined;
232
163
  }
233
164
  /**
234
- * Registers a synchronous custom token handler.
165
+ * Registers a custom token handler. Both sync and async handlers are registered via this method
166
+ * and share the same callback list.
235
167
  *
236
- * When {@link Detokeniser.do} encounters a token not handled by the built-in set, it invokes each
237
- * registered sync callback in registration order. The first to return a non-`undefined` string wins.
238
- * Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown.
168
+ * Callbacks are tried in registration order. The first to return a non-`undefined` value wins.
169
+ * Return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
170
+ * built-in handler matched, an error is thrown.
239
171
  *
240
- * @param callback - `(delimiter: string, token: string) => string | undefined`
241
- * - `delimiter`current delimiter character (default `|`)
242
- * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
172
+ * Async callbacks (those returning a `Promise`) are silently skipped when {@link Detokeniser.do}
173
+ * is used register an async callback only if you intend to call {@link Detokeniser.doAsync}.
174
+ *
175
+ * @param callback - `(token: string) => string | undefined | Promise<string | undefined>`
176
+ * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
243
177
  *
244
178
  * @example
245
- * // Handle [[env|VAR_NAME]] tokens
246
- * Detokeniser.addCallbackSync((delimiter, token) => {
247
- * const [type, name] = token.split(delimiter);
248
- * if (type.toLowerCase() !== 'env') return undefined;
179
+ * // Sync handler for [[env|VAR_NAME]] tokens
180
+ * Detokeniser.addCallback((token) => {
181
+ * const [type, name] = token.split('|');
182
+ * if (type !== 'env') return undefined;
249
183
  * return process.env[name] ?? '';
250
184
  * });
251
185
  * Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
252
186
  *
253
187
  * @example
254
- * // Multiple callbacks each handles one type, passes on the rest
255
- * Detokeniser.addCallbackSync((delimiter, token) => {
256
- * const [type, value] = token.split(delimiter);
257
- * if (type === 'upper') return value.toUpperCase();
258
- * return undefined;
259
- * });
260
- * Detokeniser.addCallbackSync((delimiter, token) => {
261
- * const [type, value] = token.split(delimiter);
262
- * if (type === 'lower') return value.toLowerCase();
263
- * return undefined;
264
- * });
265
- * Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world'
266
- *
267
- * @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks
268
- * @see {@link Detokeniser.addCallbackAsync} for async token handlers
269
- */
270
- static addCallbackSync(callback) {
271
- if (!this._syncCallbacks) {
272
- this._syncCallbacks = new Array();
273
- }
274
- this._syncCallbacks.push(callback);
275
- }
276
- /**
277
- * Registers an asynchronous custom token handler.
278
- *
279
- * Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}.
280
- * Async callbacks are tried in registration order; the first to return a non-`undefined` value wins.
281
- * Return `undefined` to pass to the next callback.
282
- *
283
- * @param asyncCallback - `(delimiter: string, token: string) => Promise<string | undefined>`
284
- * - `delimiter` — current delimiter character (default `|`)
285
- * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
286
- *
287
- * @example
288
- * // Handle [[db|table|column|whereClause]] tokens
289
- * Detokeniser.addCallbackAsync(async (delimiter, token) => {
290
- * const [type, table, column, where] = token.split(delimiter);
291
- * if (type.toLowerCase() !== 'db') return undefined;
188
+ * // Async handler for [[db|table|column|where]] tokens
189
+ * Detokeniser.addCallback(async (token) => {
190
+ * const [type, table, column, where] = token.split('|');
191
+ * if (type !== 'db') return undefined;
292
192
  * const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
293
193
  * return String(row[column]);
294
194
  * });
295
195
  * const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
296
196
  *
297
- * @example
298
- * // Combine with nested tokens inner tokens resolve before the callback is called
299
- * Detokeniser.addCallbackAsync(async (delimiter, token) => {
300
- * const [type, key] = token.split(delimiter);
301
- * if (type !== 'cache') return undefined;
302
- * return await redis.get(key);
303
- * });
304
- * // [[random|digits|8]] resolves first, then [[cache|...]] receives the result
305
- * await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]');
306
- *
307
- * @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks
308
- * @see {@link Detokeniser.addCallbackSync} for synchronous token handlers
197
+ * @see {@link Detokeniser.resetCallbacks} to remove all registered callbacks
198
+ * @see {@link Detokeniser.doAsync} for async token resolution
309
199
  */
310
- static addCallbackAsync(asyncCallback) {
311
- if (!this._asyncCallbacks) {
312
- this._asyncCallbacks = new Array();
200
+ static addCallback(callback) {
201
+ if (!this._callbacks) {
202
+ this._callbacks = new Array();
313
203
  }
314
- this._asyncCallbacks?.push(asyncCallback);
204
+ this._callbacks.push(callback);
315
205
  }
316
206
  /**
317
- * Removes all registered sync callbacks. Built-in token handlers are unaffected.
207
+ * Removes all registered callbacks. Built-in token handlers are unaffected.
318
208
  * Use between tests or scenarios to ensure callback isolation.
319
- * @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults
209
+ * @see {@link Detokeniser.reset} to also restore all defaults
320
210
  */
321
- static resetSyncCallbacks() {
322
- if (this._syncCallbacks) {
323
- this._syncCallbacks = [];
324
- }
325
- }
326
- /**
327
- * Removes all registered async callbacks. Built-in token handlers are unaffected.
328
- * Use between tests or scenarios to ensure callback isolation.
329
- * @see {@link Detokeniser.reset} to also clear sync callbacks and restore all defaults
330
- */
331
- static resetAsyncCallbacks() {
332
- if (this._asyncCallbacks) {
333
- this._asyncCallbacks = [];
334
- }
211
+ static resetCallbacks() {
212
+ this._callbacks = undefined;
335
213
  }
336
214
  /**
337
215
  * Synchronously resolves all tokens in the given string and returns the result.
@@ -418,8 +296,8 @@ export class Detokeniser {
418
296
  *
419
297
  * @example
420
298
  * // Async callback for database-driven tokens
421
- * Detokeniser.addCallbackAsync(async (delim, token) => {
422
- * const [type, key] = token.split(delim);
299
+ * Detokeniser.addCallbackAsync(async (token) => {
300
+ * const [type, key] = token.split('|');
423
301
  * if (type !== 'db') return undefined;
424
302
  * return await fetchFromDatabase(key);
425
303
  * });
@@ -553,9 +431,12 @@ export class Detokeniser {
553
431
  //
554
432
  try {
555
433
  if (Utils.isNullOrUndefined(processedToken)) {
556
- if (typeof this._syncCallbacks != "undefined") {
557
- this._syncCallbacks.every((callback) => {
558
- processedToken = callback(this._delimiter, token);
434
+ if (typeof this._callbacks != "undefined") {
435
+ this._callbacks.every((callback) => {
436
+ const result = callback(token);
437
+ if (result instanceof Promise)
438
+ return true; // skip async callbacks in sync context
439
+ processedToken = result;
559
440
  return Utils.isNullOrUndefined(processedToken);
560
441
  });
561
442
  }
@@ -586,9 +467,9 @@ export class Detokeniser {
586
467
  //
587
468
  try {
588
469
  if (Utils.isNullOrUndefined(processedToken)) {
589
- if (typeof this._asyncCallbacks != "undefined") {
590
- for (let i = 0; i < this._asyncCallbacks.length; i++) {
591
- processedToken = await this._asyncCallbacks[i](this._delimiter, token);
470
+ if (typeof this._callbacks != "undefined") {
471
+ for (const callback of this._callbacks) {
472
+ processedToken = await Promise.resolve(callback(token));
592
473
  if (!Utils.isNullOrUndefined(processedToken))
593
474
  break;
594
475
  }
@@ -834,7 +715,7 @@ export class Detokeniser {
834
715
  case "addhours":
835
716
  returnDate = addHours(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
836
717
  break;
837
- case "addMinutes":
718
+ case "addminutes":
838
719
  returnDate = addMinutes(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
839
720
  break;
840
721
  case "followingday": {
@@ -1060,8 +941,7 @@ Detokeniser._endTokenChar = "]]";
1060
941
  Detokeniser._startTokenChar = "[[";
1061
942
  Detokeniser.EscapeChar = "/";
1062
943
  Detokeniser._delimiter = "|";
1063
- Detokeniser._asyncCallbacks = undefined;
1064
- Detokeniser._syncCallbacks = undefined;
944
+ Detokeniser._callbacks = undefined;
1065
945
  class InnermostToken {
1066
946
  constructor(inputString, StartTokenChar, EndTokenChar, EscapeChar) {
1067
947
  let startIndex = -1;
package/dist/esm/index.js CHANGED
@@ -4,3 +4,4 @@ export const LogLevels = Logger.Levels;
4
4
  export { JsonUtils } from "./jsonUtils/jsonUtils";
5
5
  export { StringUtils } from "./stringUtils/stringUtils";
6
6
  export { Utils, ExistingFileWriteActions } from "./utils/utils";
7
+ export { Mock } from "./mock/mock";
@@ -485,7 +485,7 @@ export class Logger {
485
485
  const stackObj = {};
486
486
  Error.captureStackTrace(stackObj, this.writeLine);
487
487
  const stack = stackObj?.stack ?? "[Unknown]";
488
- const callingMethodDetails = this.callingMethodDetails(stack);
488
+ const callingMethodDetails = this.callingMethodDetails(stack, options?.stackOffset ?? 0);
489
489
  const maxLines = options?.maxLines ?? this.options.writeLine.maxLines;
490
490
  const suppressAllPreamble = options?.suppressAllPreamble ??
491
491
  this.options.writeLine.suppressAllPreamble;
@@ -705,7 +705,7 @@ export class Logger {
705
705
  return this.pad(levelOfWrite, WRITE_TYPE_PAD_WIDTH);
706
706
  }
707
707
  }
708
- static callingMethodDetails(methodBase) {
708
+ static callingMethodDetails(methodBase, stackOffset = 0) {
709
709
  let methodName = "<Unknown>";
710
710
  let typeName = "";
711
711
  if (methodBase) {
@@ -717,7 +717,9 @@ export class Logger {
717
717
  .findIndex((item) => !item.includes(LOGGER_STACK_FRAME_MARKER));
718
718
  indexOfFirstNonLogLine =
719
719
  indexOfFirstNonLogLine === -1 ? 1 : indexOfFirstNonLogLine + 1;
720
- methodName = methodBaseLines[indexOfFirstNonLogLine]
720
+ const safeOffset = Math.max(0, stackOffset);
721
+ const targetIndex = Math.min(indexOfFirstNonLogLine + safeOffset, methodBaseLines.length - 1);
722
+ methodName = methodBaseLines[targetIndex]
721
723
  .replace(/\s\s+/g, " ")
722
724
  .trim();
723
725
  if (methodName.startsWith("at ")) {