229 lines
5.9 KiB
TypeScript
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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|