All files / metadata/src/displayset splitRuleSchema.ts

100% Statements 12/12
100% Branches 5/5
100% Functions 1/1
100% Lines 12/12

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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218                                    137x           137x                                                                                                                                                                             137x                                                                                   137x                                     137x                           137x               26989x 9727x 9727x 26852x     26989x               137x                  
/**
 * The **split rule schema**: one table of every place in a raw display set
 * split rule, of what each place holds, and of how the compiled function of a
 * function place is called and what it returns.
 *
 * `createDisplaySetSplitRules` checks the fields, selects the form of each
 * fragment, and builds its error messages from this table, so a key that is
 * not in the table does not compile. The names in a rule are not in this
 * table: `displaySetContext.ts` checks them. See "The schema of a rule" in
 * `packages/docs/docs/concepts/cornerstone-metadata/display-sets.md`.
 *
 * @module displayset/splitRuleSchema
 */
 
import { conditionShape, matchTestShape, valueShape } from '../safeFunctions';
import type { Schema, SchemaShape } from '../safeFunctions';
 
/** The names that only `attributes` can read: values of the display set. */
export const ATTRIBUTE_ONLY_NAMES = Object.freeze([
  'splitNumber',
  'sopClassUids',
] as const);
 
/** One raw split rule. */
const ruleShape: SchemaShape = {
  name: 'rule',
  keyNoun: 'field',
  forms: {
    rule: {
      keys: {
        id: {
          kind: 'string',
          optional: true,
          description:
            'The rule id. Optional: the key of the rule in the rule set is the id. When present it must equal that key.',
        },
        groupId: {
          kind: 'string',
          optional: true,
          description:
            'The group of rules this rule belongs to, for a reader that must recognize display sets from several related rules (for example every mammography rule). Defaults to the rule id.',
        },
        priority: {
          kind: 'priority',
          description:
            'Evaluation order: ascending, first match wins. null excludes the rule.',
        },
        description: {
          kind: 'string',
          optional: true,
          description: 'Human-readable explanation. Not compiled.',
        },
        viewportTypes: {
          kind: 'string',
          list: true,
          optional: true,
          description:
            'Viewport types for the host. CS3D copies them to the display set unchanged, and does not read them.',
        },
        handler: {
          kind: 'string',
          optional: true,
          description:
            'The generic handler name of the display sets, e.g. segmentation. Any string; the host decides what an unknown name does.',
        },
        context: {
          kind: 'contextEntry',
          record: true,
          optional: true,
          description:
            'Context entries for this rule only. An entry replaces the global entry of the same name, also inside the global entries that the rule reads, or defines a new name.',
        },
        matches: {
          kind: 'condition',
          optional: true,
          function: true,
          call: '(instance, context) => boolean',
          description:
            'Which instances this rule claims: a map of name to test, and every entry must hold. Omit for a catch-all rule.',
        },
        groupBy: {
          kind: 'name',
          list: true,
          optional: true,
          call: '(instance, context) => value',
          description:
            'Names (or functions) whose values must all be equal for two instances to form one group. Defaults to SeriesInstanceUID.',
        },
        sortBy: {
          kind: 'sortKey',
          list: true,
          optional: true,
          call: '(a, b, context) => number',
          description:
            'The order of the instances in a display set: names in ascending order, named sort functions, or functions. A result of 0 or NaN is "no opinion": the next entry decides.',
        },
        attributes: {
          kind: 'attributeValue',
          record: true,
          optional: true,
          function: true,
          call: '(attributes, options) => Record<string, unknown>',
          description:
            'Attributes of the produced display sets, keyed by attribute name.',
        },
      },
    },
  },
};
 
/** One entry of the `context` option, or of the `context` of a rule. */
const contextEntryShape: SchemaShape = {
  name: 'context entry',
  string: 'expression',
  function: true,
  forms: {
    declaration: {
      selectBy: 'attribute',
      keys: {
        attribute: { kind: 'string' },
        vr: { kind: 'string', optional: true },
        vm: { kind: 'string', optional: true },
      },
      description:
        'Reads one attribute of the instance that the DICOM dictionary does not hold: a private tag as 8-digit hex, a vendor attribute, or a synthetic attribute such as imageId. vr and vm are recorded only.',
    },
    template: {
      selectBy: 'template',
      keys: { template: { kind: 'string' } },
      description: 'Text: each {name} gives the value of a name or a path.',
    },
    group: {
      selectBy: 'function',
      keys: {
        function: {
          kind: 'literal',
          description:
            'The name of a series function (planeGeometry, timeClusters, or one of options.seriesFunctions), or in code a function.',
        },
        args: {
          kind: 'literal',
          optional: true,
          description:
            'The arguments of the series function. Each value is a fixed value, or an expression with one value for the series.',
        },
      },
      description:
        'One call of a series function over the series. A rule reads a result field by path, e.g. timeClusters.start. In sortBy and attributes, displaySet.<name> runs it over the display set.',
    },
  },
};
 
/** An entry of `sortBy` that is not a name. */
const sortKeyShape: SchemaShape = {
  name: 'sort entry',
  string: 'attribute',
  function: true,
  forms: {
    sortFunction: {
      selectBy: 'sortFunction',
      keys: {
        sortFunction: {
          kind: 'string',
          description: 'The name of a sort function in options.sortFunctions.',
        },
      },
      description: 'A named sort function, called (a, b, context) => number.',
    },
  },
};
 
/** One value of `attributes`. */
const attributeValueShape: SchemaShape = {
  ...valueShape,
  name: 'attribute value',
  forms: {
    template: valueShape.forms.template,
    expression: {
      ...valueShape.forms.expression,
      description:
        'An expression over the display set: displaySet.instances.max(x) (and min, sum, count) runs over its instances, series.instances.max(x) over the series, and a DICOM keyword reads its first instance.',
    },
  },
};
 
/** A name: a DICOM keyword or a context name, or in code a function. */
const nameShape: SchemaShape = {
  name: 'name',
  string: 'attribute',
  function: true,
  forms: {},
};
 
function deepFreeze<T>(value: T): T {
  if (value && typeof value === 'object' && !Object.isFrozen(value)) {
    Object.freeze(value);
    for (const entry of Object.values(value)) {
      deepFreeze(entry);
    }
  }
  return value;
}
 
/**
 * The schema of a raw split rule, keyed by shape name. `rule` is the rule
 * itself; the other entries are the shapes its fields hold. Frozen: the
 * compiler reads it, so a change to it would change what compiles.
 */
export const splitRuleSchema = deepFreeze({
  rule: ruleShape,
  contextEntry: contextEntryShape,
  condition: conditionShape,
  matchTest: matchTestShape,
  name: nameShape,
  sortKey: sortKeyShape,
  attributeValue: attributeValueShape,
}) satisfies Schema;