@jsonjoy.com/json-type
Version:
High-performance JSON Pointer implementation
163 lines • 6.46 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Discriminator = void 0;
const classes_1 = require("./classes");
/**
* Discriminator class for automatically identifying distinguishing patterns in
* union types.
*
* This class analyzes types to find discriminatory characteristics that can be
* used to differentiate between variants in a union type at runtime. It can
* autodiscriminate:
*
* - **Constant values** (`ConType`): Exact literal values (strings, numbers, booleans, null)
* - **Primitive types**: `boolean`, `number`, `string` based on JavaScript `typeof`
* - **Structural types**: `object` vs `array` differentiation
* - **Nested discriminators**: Constant values or types found in object properties or array elements
*
* ## Discriminator Specifiers
*
* Specifiers are JSON-encoded arrays `[path, typeSpecifier, value]` that
* uniquely identify discriminators:
*
* **Constant value discriminators** (exact matches):
*
* - `["", "con", "success"]` - Root value must be string "success"
* - `["/type", "con", "user"]` - Property `type` must be string "user"
* - `["/0", "con", 42]` - First array element must be number 42
* - `["", "con", null]` - Root value must be null
*
* **Type-based discriminators** (typeof checks):
*
* - `["", "bool", 0]` - Root value must be boolean (any boolean)
* - `["/age", "num", 0]` - Property `age` must be number (any number)
* - `["/name", "str", 0]` - Property `name` must be string (any string)
* - `["", "obj", 0]` - Root value must be object
* - `["", "arr", 0]` - Root value must be array
*
* **Handling Value Types vs Constants**:
*
* - **Constant values**: When discriminator finds a `ConType`, it creates exact value matches.
* - **Value types**: When discriminator finds primitive types without constants, it matches by `typeof`.
* - **Precedence**: Constant discriminators are preferred over type discriminators for more specific matching.
*
* The discriminator creates JSON Expression conditions that can be evaluated at
* runtime to determine which type variant a value matches in a union type. JSON
* Expression can be compiled to JavaScript for efficient evaluation.
*/
class Discriminator {
static findConst(type) {
if (type instanceof classes_1.ConType) {
return new Discriminator('', type);
}
else if (type instanceof classes_1.ArrType) {
const { _head = [] } = type;
// TODO: add support for array tail.
const types = _head;
for (let i = 0; i < types.length; i++) {
const t = types[i];
const d = Discriminator.findConst(t);
if (d)
return new Discriminator('/' + i + d.path, d.type);
}
}
else if (type instanceof classes_1.ObjType) {
const fields = type.keys;
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
const d = Discriminator.findConst(f.val);
if (d)
return new Discriminator('/' + f.key + d.path, d.type);
}
}
return undefined;
}
static find(type) {
const constDiscriminator = Discriminator.findConst(type);
return constDiscriminator ?? new Discriminator('', type);
}
static createExpression(types) {
const specifiers = new Set();
const length = types.length;
const expanded = [];
const expand = (type) => {
while (type.kind() === 'ref' || type.kind() === 'key') {
if (type.kind() === 'ref')
type = type.resolve();
if (type.kind() === 'key')
type = type.val;
}
if (type.kind() === 'or')
return type.types.flatMap((t) => expand(t));
return [type];
};
for (let i = 0; i < length; i++)
expanded.push(...expand(types[i]));
const expandedLength = expanded.length;
const discriminators = [];
for (let i = 1; i < expandedLength; i++) {
const type = expanded[i];
const d = Discriminator.find(type);
const specifier = d.toSpecifier();
if (specifiers.has(specifier))
throw new Error('Duplicate discriminator: ' + specifier);
specifiers.add(specifier);
discriminators.push(d);
}
let expr = 0;
for (let i = 0; i < discriminators.length; i++) {
const d = discriminators[i];
expr = ['?', d.condition(), i + 1, expr];
}
return expr;
}
constructor(path, type) {
this.path = path;
this.type = type;
}
condition() {
if (this.type instanceof classes_1.ConType)
return ['==', this.type.literal(), ['$', this.path, this.type.literal() === null ? '' : null]];
if (this.type instanceof classes_1.BoolType)
return ['==', ['type', ['$', this.path]], 'boolean'];
if (this.type instanceof classes_1.NumType)
return ['==', ['type', ['$', this.path]], 'number'];
if (this.type instanceof classes_1.StrType)
return ['==', ['type', ['$', this.path]], 'string'];
switch (this.typeSpecifier()) {
case 'obj':
return ['==', ['type', ['$', this.path]], 'object'];
case 'arr':
return ['==', ['type', ['$', this.path]], 'array'];
}
throw new Error('Cannot create condition for discriminator: ' + this.toSpecifier());
}
typeSpecifier() {
const kind = this.type.kind();
switch (kind) {
case 'bool':
case 'str':
case 'num':
case 'con':
return kind;
case 'obj':
case 'map':
return 'obj';
case 'arr':
return 'arr';
case 'fn':
case 'fn$':
return 'fn';
}
return '';
}
toSpecifier() {
const type = this.type;
const path = this.path;
const typeSpecifier = this.typeSpecifier();
const value = type instanceof classes_1.ConType ? type.literal() : 0;
return JSON.stringify([path, typeSpecifier, value]);
}
}
exports.Discriminator = Discriminator;
//# sourceMappingURL=discriminator.js.map