All files / src/utils/common parse-lines.ts

100% Statements 21/21
100% Branches 12/12
100% Functions 3/3
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94                                                                    28x                                           24x 2x       22x 22x   64x 6x     58x 58x 58x 3x     41x   14x 14x 14x     62x   22x 17x     4x 3x     1x    
import { errorify } from '../converter/errorify.ts';
import { emptyArray } from './empty.ts';
import type { JsonSafe } from './serialization.ts';
import { jsonParse } from './serialization.ts';
 
/**
 * Parses each line of the specified value as a full serialized JSON value, returning all values as an array. This
 * method is useful when, for example, handling
 * [NDJSON](https://en.wikipedia.org/wiki/JSON_streaming#Newline-delimited_JSON) log entries.
 *
 * By default this method only throws when an error occurs while parsing a line _and_ no line yields contains a valid
 * serialized JSON value - in other words, if the parsing of a single line succeeds, all errors are ignored. This
 * behavior can be modified by using `options.onError` which is notified for every error and can be implemented to throw
 * a value that is then propagated to the caller.
 *
 * @example
 *
 * ```ts
 * import { parseLines } from 'emitnlog/logger';
 *
 * const value = [
 *   '{"level":"info","timestamp":1705312245123,"message":"Application started"}',
 *   '{"level":"error","timestamp":1705312246456,"message":"Connection failed","args":[{"host":"db.example.com"}]}',
 * ].join('\n');
 *
 * // Parses 2 objects
 * const objects = parseLines(value);
 * ```
 *
 * @param value A string containing one or more JSON objects, each on a separate line.
 * @param options Optional options for the parsing process.
 * @returns An array with the parsed objects or an empty array if there were no objects in the value.
 * @throws An error if is there are no objects in the value and an error were thrown or if `options.onError` throws.
 */
export const parseLines = <T = unknown>(
  value: string,
  options?: {
    /**
     * Callback invoked whenever an error occurs while parsing a line.
     *
     * Any value thrown by this method interrupts the parsing and is propagated to the caller.
     *
     * @param error The error thrown
     * @param line The line that caused the error
     */
    readonly onError?: (error: Error, line: string) => void;
 
    /**
     * By default parsed `null` values are ignored. Setting this option to true causes them to be added to the returned
     * array and considered as valid JSON values.
     *
     * @default false
     */
    readonly keepNull?: boolean;
  },
): readonly JsonSafe<T>[] => {
  if (!value.trim()) {
    return emptyArray();
  }
 
  let error: Error | undefined;
  const lines = value.split('\n');
  const objects = lines
    .map((line) => {
      if (!line.trim()) {
        return undefined;
      }
 
      try {
        const parsed = jsonParse<T>(line);
        if (parsed === null && !options?.keepNull) {
          return undefined;
        }
 
        return parsed;
      } catch (e: unknown) {
        error = errorify(e);
        options?.onError?.(error, line);
        return undefined;
      }
    })
    .filter((item): item is JsonSafe<T> => item !== undefined);
 
  if (objects.length) {
    return objects;
  }
 
  if (error) {
    throw error;
  }
 
  return emptyArray();
};