2021-03-11 16:59:40 +00:00
|
|
|
import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Value, Position, position, ReaderOptions, stringify } from '@preserves/core';
|
2021-03-09 14:59:40 +00:00
|
|
|
import { Input, NamedPattern, Pattern, Schema } from './meta';
|
|
|
|
import * as M from './meta';
|
2021-03-11 16:59:40 +00:00
|
|
|
import { SchemaSyntaxError } from './error';
|
2021-03-09 14:59:40 +00:00
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
function invalidClause(clause: Array<Input>): never {
|
2021-03-11 16:59:40 +00:00
|
|
|
throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`,
|
|
|
|
position(clause[0] ?? false));
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
function invalidPattern(name: string, item: Input, pos: Position | null): never {
|
|
|
|
throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 22:02:18 +00:00
|
|
|
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 ?? {});
|
2021-03-09 15:45:57 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 22:02:18 +00:00
|
|
|
export function parseSchema(toplevelTokens: Array<Input>,
|
|
|
|
options: ReaderOptions<never> & SchemaReaderOptions): Schema
|
|
|
|
{
|
2021-03-10 22:15:53 +00:00
|
|
|
let version: M.Version | undefined = void 0;
|
2021-03-11 16:59:40 +00:00
|
|
|
let pointer: M.PointerName = false;
|
2021-03-10 22:15:53 +00:00
|
|
|
let definitions = new Dictionary<Pattern, never>();
|
2021-03-11 22:02:18 +00:00
|
|
|
|
|
|
|
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);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-11 22:02:18 +00:00
|
|
|
|
|
|
|
process(toplevelTokens);
|
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
if (version === void 0) {
|
2021-03-11 16:59:40 +00:00
|
|
|
throw new SchemaSyntaxError("Schema: missing version declaration.", null);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
2021-03-11 22:02:18 +00:00
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
return M.asSchema(Record(M.$schema, [new Dictionary<Value>([
|
2021-03-10 22:15:53 +00:00
|
|
|
[M.$version, version],
|
2021-03-11 16:59:40 +00:00
|
|
|
[M.$pointer, pointer],
|
2021-03-10 22:15:53 +00:00
|
|
|
[M.$definitions, definitions],
|
2021-03-11 16:59:40 +00:00
|
|
|
])]));
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function parseDefinition(name: symbol, body: Array<Input>): Pattern {
|
|
|
|
return parseOp(body, M.ORSYM, p => parseOp(p, M.ANDSYM, p => parseBase(name, p)));
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseOp(body: Array<Input>, op: Input, k: (p: Array<Input>) => Pattern): Pattern {
|
|
|
|
const pieces = splitBy(body, op);
|
|
|
|
if (pieces.length === 1) return k(pieces[0]);
|
|
|
|
switch (op) {
|
2021-03-10 22:15:53 +00:00
|
|
|
case M.ORSYM: return Record(M.$or, [pieces.map(k)]);
|
|
|
|
case M.ANDSYM: return Record(M.$and, [pieces.map(k)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
default: throw new Error("Internal error: unexpected operator");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
function parseRef(name: string, pos: Position | null, item: symbol): Pattern {
|
|
|
|
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('.');
|
|
|
|
if (pieces.length === 1) {
|
|
|
|
return recordPosition(Record(M.$ref, [M.$thisModule, item]), pos);
|
|
|
|
} else {
|
|
|
|
return recordPosition(Record(M.$ref, [
|
|
|
|
pieces.slice(0, pieces.length - 1).map(Symbol.for),
|
|
|
|
Symbol.for(pieces[pieces.length - 1])
|
|
|
|
]), pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
function parseBase(name: symbol, body: Array<Input>): Pattern {
|
|
|
|
body = peel(body) as Array<Input>;
|
2021-03-11 13:43:06 +00:00
|
|
|
if (body.length !== 1) {
|
2021-03-11 16:59:40 +00:00
|
|
|
invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body));
|
2021-03-11 13:43:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const pos = position(body[0]);
|
2021-03-09 14:59:40 +00:00
|
|
|
const item = peel(body[0]);
|
2021-03-11 13:43:06 +00:00
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
const walk = (b: Input): Pattern => parseBase(name, [b]);
|
|
|
|
const namedwalk = (b: Input): NamedPattern => {
|
|
|
|
const name = findName(b);
|
|
|
|
if (name === false) return walk(b);
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$named, [name, walk(b)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
};
|
|
|
|
const walkitems = (b: Input): Pattern[] => {
|
|
|
|
b = peel(b);
|
|
|
|
if (!Array.isArray(b)) complain();
|
|
|
|
return b.map(walk);
|
|
|
|
};
|
|
|
|
|
|
|
|
function complain(): never {
|
2021-03-11 16:59:40 +00:00
|
|
|
invalidPattern(stringify(name), item, pos);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof item === 'symbol') {
|
2021-03-11 16:59:40 +00:00
|
|
|
return parseRef(stringify(name), position(body[0]), item);
|
2021-03-09 14:59:40 +00:00
|
|
|
} 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) {
|
2021-03-10 22:15:53 +00:00
|
|
|
case M.$lit:
|
2021-03-09 14:59:40 +00:00
|
|
|
if (item.length !== 1) complain();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$lit, [item[0]]);
|
|
|
|
case M.$or:
|
2021-03-09 14:59:40 +00:00
|
|
|
if (item.length !== 1) complain();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$or, [walkitems(item[0])]);
|
|
|
|
case M.$and:
|
2021-03-09 14:59:40 +00:00
|
|
|
if (item.length !== 1) complain();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$and, [walkitems(item[0])]);
|
|
|
|
case M.$rec:
|
2021-03-09 14:59:40 +00:00
|
|
|
if (item.length !== 2) complain();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$rec, [walk(item[0]), walk(item[1])]);
|
2021-03-09 14:59:40 +00:00
|
|
|
default:
|
|
|
|
complain();
|
|
|
|
}
|
|
|
|
} else {
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$rec, [Record(M.$lit, [label]), Record(M.$tuple, [item.map(namedwalk)])]);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
} else if (Array.isArray(item)) {
|
|
|
|
if (is(item[item.length - 1], M.DOTDOTDOT)) {
|
|
|
|
if (item.length < 2) complain();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$tuple_STAR_, [
|
2021-03-09 14:59:40 +00:00
|
|
|
item.slice(0, item.length - 2).map(namedwalk),
|
|
|
|
namedwalk(item[item.length - 2]),
|
|
|
|
]);
|
|
|
|
} else {
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$tuple, [item.map(namedwalk)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
} else if (Dictionary.isDictionary<Input, never>(item)) {
|
|
|
|
if (item.size === 2 && item.has(M.DOTDOTDOT)) {
|
|
|
|
const v = item.clone();
|
|
|
|
v.delete(M.DOTDOTDOT);
|
|
|
|
const [[kp, vp]] = v.entries();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$dictof, [walk(kp), walk(vp)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
} else {
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$dict, [item.mapEntries<Pattern, Input, never>(([k, vp]) =>
|
2021-03-09 14:59:40 +00:00
|
|
|
[strip(k), walk(vp)])]);
|
|
|
|
}
|
|
|
|
} else if (Set.isSet<never>(item)) {
|
|
|
|
if (item.size !== 1) complain();
|
|
|
|
const [vp] = item.entries();
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$setof, [walk(vp)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
} else {
|
2021-03-10 22:15:53 +00:00
|
|
|
return Record(M.$lit, [strip(item)]);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
}
|