All files / metadata/src/displayset splitRuleSet.ts

25.92% Statements 7/27
34.48% Branches 10/29
16.66% Functions 1/6
25.92% Lines 7/27

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 95 96 97 98 99 100 101 102 103                                                2192x     2192x     2192x           2192x 2192x     2192x           2192x                                                                                                                
import { isUnsafeKey } from '../safeFunctions/schema';
import type { SplitRule, SplitRuleSet } from './types';
 
/**
 * Throws with the offending entry named - a rule set is usually merged from
 * several customization layers, so the message must say which rule is wrong.
 */
function invalidEntry(id: string, message: string): never {
  throw new Error(`Invalid split rule set entry "${id}": ${message}`);
}
 
/**
 * Checks one rule set entry, and says whether it is included.
 *
 * @returns false for an entry with a `null` priority, which excludes it.
 * @throws if the entry is not an object, has a priority that is neither `null`
 *   nor a finite number, or states an `id` that differs from its key.
 */
export function validateSplitRuleSetEntry(
  id: string,
  entry: { id?: string; priority?: number | null } | null | undefined
): boolean {
  // A rule set is a plain object keyed by id, and `rules['__proto__'] = rule`
  // replaces the prototype of the rule set instead of adding the rule.
  Iif (isUnsafeKey(id)) {
    invalidEntry(id, 'the id is not allowed; give the rule another id');
  }
  Iif (!entry || typeof entry !== 'object') {
    invalidEntry(id, 'the entry must be an object');
  }
  Iif (entry.id !== undefined && entry.id !== id) {
    invalidEntry(
      id,
      `the entry states id "${entry.id}", but its key is the id. Remove the id, or make it equal to the key.`
    );
  }
  const { priority } = entry;
  Iif (priority === null) {
    return false;
  }
  Iif (typeof priority !== 'number' || !Number.isFinite(priority)) {
    invalidEntry(
      id,
      `priority must be a finite number, or null to exclude the rule; got ${JSON.stringify(priority)}`
    );
  }
  return true;
}
 
/**
 * The included entries of a keyed rule set, in evaluation order, each with its
 * key as its `id` and without its `priority`.
 *
 * - A `null` priority excludes the rule.
 * - Rules are ordered by ascending priority. Equal priorities are ordered by id
 *   (UTF-16 code unit order, not a locale collation), so the result never
 *   depends on the order the keys were inserted in.
 *
 * Generic over the entry shape so that the rules as data and the compiled
 * rules share one ordering implementation.
 *
 * @throws see {@link validateSplitRuleSetEntry}.
 */
export function orderSplitRuleSetEntries<
  Entry extends { id?: string; priority?: number | null },
>(
  ruleSet: Record<string, Entry>
): (Omit<Entry, 'priority'> & { id: string })[] {
  if (!ruleSet || typeof ruleSet !== 'object' || Array.isArray(ruleSet)) {
    throw new Error(
      `A split rule set must be an object keyed by rule id: ${JSON.stringify(ruleSet)}`
    );
  }
 
  const included: { id: string; priority: number; entry: Entry }[] = [];
  for (const [id, entry] of Object.entries(ruleSet)) {
    if (validateSplitRuleSetEntry(id, entry)) {
      included.push({ id, priority: entry.priority as number, entry });
    }
  }
 
  included.sort((a, b) => {
    if (a.priority !== b.priority) {
      return a.priority - b.priority;
    }
    return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
  });
 
  return included.map(({ id, entry }) => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { priority, ...rule } = entry;
    return { ...rule, id };
  });
}
 
/**
 * The rules of a {@link SplitRuleSet} in evaluation order - the order the split
 * engine tries them in. See {@link orderSplitRuleSetEntries}.
 */
export function resolveSplitRuleSet(ruleSet: SplitRuleSet): SplitRule[] {
  return orderSplitRuleSetEntries(ruleSet) as SplitRule[];
}