inline-schema/src/validator.ts

229 lines
5.9 KiB
TypeScript

import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema } from './types';
import { ParseError } from './parser';
class ValueParser {
private input: string;
private pos: number = 0;
constructor(input: string) {
this.input = input;
}
private peek(): string {
return this.input[this.pos] || '';
}
private consume(): string {
return this.input[this.pos++] || '';
}
private skipWhitespace(): void {
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
this.pos++;
}
}
private consumeStr(str: string): boolean {
if (this.input.slice(this.pos, this.pos + str.length) === str) {
this.pos += str.length;
return true;
}
return false;
}
parseValue(schema: Schema, allowOmitBrackets: boolean = false): unknown {
this.skipWhitespace();
switch (schema.type) {
case 'string':
return this.parseStringValue();
case 'number':
return this.parseNumberValue();
case 'boolean':
return this.parseBooleanValue();
case 'tuple':
return this.parseTupleValue(schema, allowOmitBrackets);
case 'array':
return this.parseArrayValue(schema, allowOmitBrackets);
default:
throw new ParseError(`Unknown schema type: ${(schema as { type: string }).type}`, this.pos);
}
}
private parseStringValue(): string {
let result = '';
while (this.pos < this.input.length) {
const char = this.peek();
if (char === '\\') {
this.consume();
const nextChar = this.consume();
if (nextChar === ';' || nextChar === '[' || nextChar === ']' || nextChar === '\\') {
result += nextChar;
} else {
result += '\\' + nextChar;
}
} else if (char === ';' || char === ']') {
break;
} else {
result += this.consume();
}
}
return result.trim();
}
private parseNumberValue(): number {
let numStr = '';
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
numStr += this.consume();
}
const num = parseFloat(numStr);
if (isNaN(num)) {
throw new ParseError('Invalid number', this.pos - numStr.length);
}
return num;
}
private parseBooleanValue(): boolean {
if (this.consumeStr('true')) {
return true;
}
if (this.consumeStr('false')) {
return false;
}
throw new ParseError('Expected true or false', this.pos);
}
private parseTupleValue(schema: TupleSchema, allowOmitBrackets: boolean): unknown[] {
let hasOpenBracket = false;
if (this.peek() === '[') {
this.consume();
hasOpenBracket = true;
} else if (!allowOmitBrackets) {
throw new ParseError('Expected [', this.pos);
}
this.skipWhitespace();
if (this.peek() === ']' && hasOpenBracket) {
this.consume();
return [];
}
const result: unknown[] = [];
for (let i = 0; i < schema.elements.length; i++) {
this.skipWhitespace();
result.push(this.parseValue(schema.elements[i], false));
this.skipWhitespace();
if (i < schema.elements.length - 1) {
if (!this.consumeStr(';')) {
throw new ParseError('Expected ;', this.pos);
}
}
}
this.skipWhitespace();
if (hasOpenBracket) {
if (!this.consumeStr(']')) {
throw new ParseError('Expected ]', this.pos);
}
}
return result;
}
private parseArrayValue(schema: ArraySchema, allowOmitBrackets: boolean): unknown[] {
let hasOpenBracket = false;
const elementIsTupleOrArray = schema.element.type === 'tuple' || schema.element.type === 'array';
if (this.peek() === '[') {
if (!elementIsTupleOrArray) {
this.consume();
hasOpenBracket = true;
} else if (this.input[this.pos + 1] === '[') {
this.consume();
hasOpenBracket = true;
}
}
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
throw new ParseError('Expected [', this.pos);
}
this.skipWhitespace();
if (this.peek() === ']' && hasOpenBracket) {
this.consume();
return [];
}
const result: unknown[] = [];
while (true) {
this.skipWhitespace();
result.push(this.parseValue(schema.element, false));
this.skipWhitespace();
if (!this.consumeStr(';')) {
break;
}
}
this.skipWhitespace();
if (hasOpenBracket) {
if (!this.consumeStr(']')) {
throw new ParseError('Expected ]', this.pos);
}
}
return result;
}
getPosition(): number {
return this.pos;
}
getInputLength(): number {
return this.input.length;
}
}
export function parseValue(schema: Schema, valueString: string): unknown {
const parser = new ValueParser(valueString.trim());
const allowOmitBrackets = schema.type === 'tuple' || schema.type === 'array';
const value = parser.parseValue(schema, allowOmitBrackets);
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError('Unexpected input after value', parser.getPosition());
}
return value;
}
export function createValidator(schema: Schema): (value: unknown) => boolean {
return function validate(value: unknown): boolean {
switch (schema.type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'tuple':
if (!Array.isArray(value)) return false;
if (value.length !== schema.elements.length) return false;
return schema.elements.every((elementSchema, index) =>
createValidator(elementSchema)(value[index])
);
case 'array':
if (!Array.isArray(value)) return false;
return value.every((item) => createValidator(schema.element)(item));
default:
return false;
}
};
}