import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Position, position, ReaderOptions, stringify } from '@preserves/core'; import { Input, NamedPattern, Pattern, Schema, Alternative, Definition } from './meta'; import * as M from './meta'; import { SchemaSyntaxError } from './error'; const positionTable = new WeakMap(); export function recordPosition(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(items: Array, separator: T): Array> { const groups: Array> = []; let group: Array = []; 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): 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): Array { return new Reader(source, { ... options ?? {}, includeAnnotations: true }).readToEnd(); } export function readSchema(source: string, options?: ReaderOptions & SchemaReaderOptions): Schema { return parseSchema(_readSchema(source, options), options ?? {}); } export function parseSchema(toplevelTokens: Array, options: ReaderOptions & SchemaReaderOptions): Schema { let version: M.Version | undefined = void 0; let pointer: M.PointerName = false; let definitions = new Dictionary(); function process(toplevelTokens: Array): void { const toplevelClauses = splitBy(peel(toplevelTokens) as Array, 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([ [M.$version, version], [M.$pointer, pointer], [M.$definitions, definitions], ])])); } function parseDefinition(name: symbol, body: Array): Definition { let nextAnonymousFieldNumber = 0; function alternativeName([input, p]: readonly [Array, Alternative]) : [symbol, Alternative] { const n = findName(input) || findName(input[0]); if (n !== false) { return [n, p]; } if (p.label === M.$rec && p[0].label === M.$lit && typeof p[0][0] === 'symbol') { return [p[0][0], p]; } if (p.label === M.$ref) { return [p[1], p]; } if (p.label === M.$lit && typeof p[0] === 'symbol') { return [p[0], p]; } return [Symbol.for('_anonymous' + nextAnonymousFieldNumber++), p]; } return parseOp(body, M.ORSYM, p => [p, parseOp(p, M.ANDSYM, p => parseBase(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 parseOp(body: Array, op: Input, each: (p: Array) => Each, combineN: (ps: Array) => 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(x)) return false; for (const a0 of x.annotations) { const a = peel(a0); if (typeof a === 'symbol') return a; } return false; } function namedwrap(f: (b: Input) => Pattern): (b: Input) => NamedPattern { return (b: Input) => { const name = findName(b); if (name === false) return f(b); return Record(M.$named, [name, f(b)]); } } 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('.'); return recordPosition(Record(M.$ref, [ pieces.slice(0, pieces.length - 1).map(Symbol.for), Symbol.for(pieces[pieces.length - 1]) ]), pos); } function parseBase(name: symbol, body: Array): Pattern { body = peel(body) as Array; if (body.length !== 1) { invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body)); } const pos = position(body[0]); const item = peel(body[0]); const walk = (b: Input): Pattern => parseBase(name, [b]); function complain(): never { invalidPattern(stringify(name), item, pos); } if (typeof item === 'symbol') { return parseRef(stringify(name), position(body[0]), item); } else if (Record.isRecord, never>(item)) { const label = item.label; if (Record.isRecord(label)) { if (label.length !== 0) complain(); switch (label.label) { case M.$lit: if (item.length !== 1) complain(); return Record(M.$lit, [item[0]]); 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(namedwrap(walk))])]); } } 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(namedwrap(walk)), namedwrap(walk)(item[item.length - 2]), ]); } else { return Record(M.$tuple, [item.map(namedwrap(walk))]); } } else if (Dictionary.isDictionary(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, [walk(kp), walk(vp)]); } else { return Record(M.$dict, [item.mapEntries(([k, vp]) => [strip(k), walk(vp)])]); } } else if (Set.isSet(item)) { if (item.size !== 1) complain(); const [vp] = item.entries(); return Record(M.$setof, [walk(vp)]); } else { return Record(M.$lit, [strip(item)]); } }