preserves/implementations/javascript/packages/schema/src/reader.ts

304 lines
12 KiB
TypeScript

import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Position, position, ReaderOptions, stringify, isCompound } from '@preserves/core';
import { Input, Pattern, Schema, Alternative, Definition, CompoundPattern, SimplePattern } from './meta';
import * as M from './meta';
import { SchemaSyntaxError } from './error';
const positionTable = new WeakMap<Input & object, Position>();
export function recordPosition<X extends Input & object>(v: X, pos: Position | null): X {
if (pos === null) { console.error('Internal error in Schema reader: null source position for', v); }
if (pos !== null) positionTable.set(v, pos);
return v;
}
export function refPosition(v: Input & object): Position | null {
return positionTable.get(v) ?? null;
}
function splitBy<T>(items: Array<T>, separator: T): Array<Array<T>> {
const groups: Array<Array<T>> = [];
let group: Array<T> = [];
function finish() {
if (group.length > 0) {
groups.push(group);
group = [];
}
}
for (const item of items) {
if (is(item, separator)) {
finish();
} else {
group.push(item);
}
}
finish();
return groups;
}
function invalidClause(clause: Array<Input>): never {
throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`,
position(clause[0] ?? false));
}
function invalidPattern(name: string, item: Input, pos: Position | null): never {
throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos);
}
export type SchemaReaderOptions = {
readInclude?(includePath: string): string;
};
function _readSchema(source: string, options?: ReaderOptions<never>): Array<Input> {
return new Reader<never>(source, {
... options ?? {},
includeAnnotations: true
}).readToEnd();
}
export function readSchema(source: string,
options?: ReaderOptions<never> & SchemaReaderOptions): Schema
{
return parseSchema(_readSchema(source, options), options ?? {});
}
export function parseSchema(toplevelTokens: Array<Input>,
options: ReaderOptions<never> & SchemaReaderOptions): Schema
{
let version: M.Version | undefined = void 0;
let pointer: M.PointerName = false;
let definitions = new Dictionary<never, Definition>();
function process(toplevelTokens: Array<Input>): void {
const toplevelClauses = splitBy(peel(toplevelTokens) as Array<Input>, M.DOT);
for (const clause of toplevelClauses) {
if (!Array.isArray(clause)) {
invalidClause(clause);
} else if (clause.length >= 2 && is(clause[1], M.EQUALS)) {
const pos = position(clause[0]);
const name = peel(clause[0]);
if (typeof name !== 'symbol') invalidClause(clause);
if (!M.isValidToken(name.description!)) {
throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, pos);
}
if (definitions.has(name)) {
throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, pos);
}
definitions.set(name, parseDefinition(name, clause.slice(2)));
} else if (clause.length === 2 && is(clause[0], M.$version)) {
version = M.asVersion(peel(clause[1]));
} else if (clause.length === 2 && is(clause[0], M.$pointer)) {
const pos = position(clause[1]);
const stx = peel(clause[1]);
const quasiName = 'pointer name specification';
pointer = M.asPointerName((stx === false) ? stx
: (typeof stx === 'symbol') ? parseRef(quasiName, pos, stx)
: invalidPattern(quasiName, stx, pos));
} else if (clause.length === 2 && is(clause[0], M.INCLUDE)) {
const pos = position(clause[1]);
const path = peel(clause[1]);
if (typeof path !== 'string') {
throw new SchemaSyntaxError(preserves`Invalid include: ${clause}`, pos);
}
if (options.readInclude === void 0) {
throw new SchemaSyntaxError(preserves`Cannot include files in schema`, pos);
}
process(_readSchema(options.readInclude(path), options));
} else {
invalidClause(clause);
}
}
}
process(toplevelTokens);
if (version === void 0) {
throw new SchemaSyntaxError("Schema: missing version declaration.", null);
}
return M.asSchema(Record(M.$schema, [new Dictionary<never>([
[M.$version, version],
[M.$pointer, pointer],
[M.$definitions, definitions],
])]));
}
function parseDefinition(name: symbol, body: Array<Input>): Definition {
let nextAnonymousAlternativeNumber = 0;
function alternativeName([input, p]: readonly [Array<Input>, Alternative])
: [string, Alternative]
{
const n = findName(input) || findName(input[0]);
if (n !== false) {
return [n.description!, p];
}
if (p.label === M.$rec && p[0].label === M.$lit && typeof p[0][0] === 'symbol') {
return [p[0][0].description!, p];
}
if (p.label === M.$ref) {
return [p[1].description!, p];
}
if (p.label === M.$lit) {
switch (typeof p[0]) {
case 'symbol': return [p[0].description!, p];
case 'string': return [p[0], p];
case 'boolean':
case 'number':
return ['' + p[0], p];
default:
break;
}
}
return ['_anonymous' + nextAnonymousAlternativeNumber++, p];
}
return parseOp(body,
M.ORSYM,
p => [p, parseOp(p,
M.ANDSYM,
p => parsePattern(name, p),
ps => Record(M.$and, [ps]),
p => p as Alternative)] as const,
ps => Record(M.$or, [ps.map(alternativeName)]),
p => p[1] as Definition);
}
function parsePattern(name: symbol, body0: Array<Input>): Pattern {
function parseSimple<A>(item0: Input, kf: () => A): SimplePattern | A {
const pos = position(item0);
const item = peel(item0);
function complain(): never { invalidPattern(stringify(name), item, pos); }
if (typeof item === 'symbol') {
return parseRef(stringify(name), pos, item);
} else if (Record.isRecord<Input, Tuple<Input>, never>(item)) {
const label = item.label;
if (Record.isRecord<Input, [], never>(label)) {
if (label.length !== 0) complain();
switch (label.label) {
case M.$lit:
if (item.length !== 1) complain();
return Record(M.$lit, [item[0]]);
default:
return kf();
}
} else {
return kf();
}
} else if (isCompound(item)) {
return kf();
} else {
return Record(M.$lit, [strip(item)]);
}
}
function parseCompound(item0: Input): CompoundPattern {
const pos = position(item0);
const item = peel(item0);
function complain(): never { invalidPattern(stringify(name), item, pos); }
const walkSimple = (b: Input): SimplePattern => parseSimple(b, () => {
throw new SchemaSyntaxError(`Compound patterns not accepted here`, position(b));
});
const walk = (b: Input): Pattern => parsePattern(name, [b]);
function _maybeNamed<R>(
recur: (b: Input) => R,
literalName?: Input): (b: Input) => M.NamedSimplePattern_ | R
{
return (b: Input) => {
let name = findName(b);
if (name === false) {
if (literalName !== void 0 && typeof literalName === 'symbol') {
name = literalName;
}
}
if (name === false) {
return recur(b);
}
return Record(M.$named, [name, parseSimple(b, () => {
throw new SchemaSyntaxError(`Named patterns must be Simple patterns`, position(b));
})]);
};
}
const maybeNamed = _maybeNamed(walk);
const maybeNamedSimple = _maybeNamed(walkSimple);
if (Record.isRecord<Input, Tuple<Input>, never>(item)) {
const label = item.label;
if (Record.isRecord<Input, [], never>(label)) {
if (label.length !== 0) complain();
switch (label.label) {
case M.$rec:
if (item.length !== 2) complain();
return Record(M.$rec, [walk(item[0]), walk(item[1])]);
default:
complain();
}
} else {
return Record(M.$rec, [Record(M.$lit, [label]), Record(M.$tuple, [item.map(maybeNamed)])]);
}
} else if (Array.isArray(item)) {
if (is(item[item.length - 1], M.DOTDOTDOT)) {
if (item.length < 2) complain();
return Record(M.$tuple_STAR_, [
item.slice(0, item.length - 2).map(maybeNamed),
maybeNamedSimple(item[item.length - 2]),
]);
} else {
return Record(M.$tuple, [item.map(maybeNamed)]);
}
} else if (Dictionary.isDictionary<never, Input>(item)) {
if (item.size === 2 && item.has(M.DOTDOTDOT)) {
const v = item.clone();
v.delete(M.DOTDOTDOT);
const [[kp, vp]] = v.entries();
return Record(M.$dictof, [walkSimple(kp), walkSimple(vp)]);
} else {
return Record(M.$dict, [item.mapEntries<M.NamedSimplePattern, Input, never>(
([k, vp]) => [strip(k), _maybeNamed(walkSimple, k)(vp)])]);
}
} else if (Set.isSet<never>(item)) {
if (item.size !== 1) complain();
const [vp] = item.entries();
return Record(M.$setof, [walkSimple(vp)]);
} else {
complain();
}
}
const body = peel(body0) as Array<Input>;
if (body.length !== 1) {
invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body));
}
return parseSimple(body[0], () => parseCompound(body[0]));
}
function parseOp<Each, Combined>(body: Array<Input>,
op: Input,
each: (p: Array<Input>) => Each,
combineN: (ps: Array<Each>) => Combined,
finish1: (p: Each) => Combined): Combined
{
const pieces = splitBy(body, op).map(each);
return (pieces.length === 1) ? finish1(pieces[0]) : combineN(pieces);
}
function findName(x: Input): symbol | false {
if (!Annotated.isAnnotated<never>(x)) return false;
for (const a0 of x.annotations) {
const a = peel(a0);
if (typeof a === 'symbol') return a;
}
return false;
}
function parseRef(name: string, pos: Position | null, item: symbol): SimplePattern {
const s = item.description;
if (s === void 0) invalidPattern(name, item, pos);
if (s[0] === '=') return Record(M.$lit, [Symbol.for(s.slice(1))]);
const pieces = s.split('.');
return recordPosition(Record(M.$ref, [
pieces.slice(0, pieces.length - 1).map(Symbol.for),
Symbol.for(pieces[pieces.length - 1])
]), pos);
}