init: inline-schema thing
This commit is contained in:
commit
4296c2bdcd
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
# inline-schema
|
||||||
|
|
||||||
|
A TypeScript library for parsing and validating inline schemas with a TypeScript-like syntax using `;` instead of `,`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install inline-schema
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineSchema } from 'inline-schema';
|
||||||
|
|
||||||
|
// Define a schema
|
||||||
|
const stringSchema = defineSchema('string');
|
||||||
|
const numberSchema = defineSchema('number');
|
||||||
|
const booleanSchema = defineSchema('boolean');
|
||||||
|
|
||||||
|
// Parse values
|
||||||
|
const name = stringSchema.parse('hello'); // "hello"
|
||||||
|
const age = numberSchema.parse('42'); // 42
|
||||||
|
const active = booleanSchema.parse('true'); // true
|
||||||
|
|
||||||
|
// Validate parsed values
|
||||||
|
stringSchema.validator(name); // true
|
||||||
|
numberSchema.validator(name); // false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tuples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tupleSchema = defineSchema('[string; number; boolean]');
|
||||||
|
|
||||||
|
// With brackets
|
||||||
|
const value1 = tupleSchema.parse('[hello; 42; true]');
|
||||||
|
// ["hello", 42, true]
|
||||||
|
|
||||||
|
// Without brackets (outermost brackets are optional)
|
||||||
|
const value2 = tupleSchema.parse('hello; 42; true');
|
||||||
|
// ["hello", 42, true]
|
||||||
|
|
||||||
|
tupleSchema.validator(value1); // true
|
||||||
|
tupleSchema.validator(['a', 'b', true]); // false (second element should be number)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrays
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Array syntax: Type[] or [Type][]
|
||||||
|
const stringArray = defineSchema('string[]');
|
||||||
|
const numberArray = defineSchema('[number][]');
|
||||||
|
|
||||||
|
// With brackets
|
||||||
|
const names1 = stringArray.parse('[alice; bob; charlie]');
|
||||||
|
// ["alice", "bob", "charlie"]
|
||||||
|
|
||||||
|
// Without brackets (outermost brackets are optional)
|
||||||
|
const names2 = stringArray.parse('alice; bob; charlie');
|
||||||
|
// ["alice", "bob", "charlie"]
|
||||||
|
|
||||||
|
const numbers = numberArray.parse('[1; 2; 3; 4; 5]');
|
||||||
|
// [1, 2, 3, 4, 5]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array of Tuples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = defineSchema('[string; number][]');
|
||||||
|
|
||||||
|
// With outer brackets
|
||||||
|
const data1 = schema.parse('[[a; 1]; [b; 2]; [c; 3]]');
|
||||||
|
// [["a", 1], ["b", 2], ["c", 3]]
|
||||||
|
|
||||||
|
// Without outer brackets
|
||||||
|
const data2 = schema.parse('[a; 1]; [b; 2]; [c; 3]');
|
||||||
|
// [["a", 1], ["b", 2], ["c", 3]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escaping Special Characters
|
||||||
|
|
||||||
|
Use `\` to escape special characters `;`, `[`, `]`, and `\` in string values:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = defineSchema('string');
|
||||||
|
|
||||||
|
const value1 = schema.parse('hello\\;world'); // "hello;world"
|
||||||
|
const value2 = schema.parse('hello\\[world'); // "hello[world"
|
||||||
|
const value3 = schema.parse('hello\\\\world'); // "hello\\world"
|
||||||
|
|
||||||
|
// In tuples
|
||||||
|
const tupleSchema = defineSchema('[string; string]');
|
||||||
|
const tuple = tupleSchema.parse('hello\\;world; test');
|
||||||
|
// ["hello;world", "test"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Identifiers
|
||||||
|
|
||||||
|
Any identifier (including hyphens) is treated as a string schema:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = defineSchema('word-smith');
|
||||||
|
const value = schema.parse('word-smith');
|
||||||
|
// "word-smith"
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `defineSchema(schemaString: string): ParsedSchema`
|
||||||
|
|
||||||
|
Parses a schema string and returns an object with:
|
||||||
|
- `schema`: The parsed schema AST
|
||||||
|
- `validator`: A function to validate values against the schema
|
||||||
|
- `parse`: A function to parse value strings
|
||||||
|
|
||||||
|
### `parseSchema(schemaString: string): Schema`
|
||||||
|
|
||||||
|
Parses a schema string and returns the schema AST.
|
||||||
|
|
||||||
|
### `parseValue(schema: Schema, valueString: string): unknown`
|
||||||
|
|
||||||
|
Parses a value string according to the given schema.
|
||||||
|
|
||||||
|
### `createValidator(schema: Schema): (value: unknown) => boolean`
|
||||||
|
|
||||||
|
Creates a validation function for the given schema.
|
||||||
|
|
||||||
|
## Schema Syntax
|
||||||
|
|
||||||
|
| Type | Schema | Example Value |
|
||||||
|
|------|--------|---------------|
|
||||||
|
| String | `string` or `identifier` | `hello` |
|
||||||
|
| Number | `number` | `42` |
|
||||||
|
| Boolean | `boolean` | `true` or `false` |
|
||||||
|
| Tuple | `[Type1; Type2; ...]` | `[hello; 42; true]` or `hello; 42; true` |
|
||||||
|
| Array | `Type[]` or `[Type][]` | `[1; 2; 3]` or `1; 2; 3` |
|
||||||
|
| Array of Tuples | `[Type1; Type2][]` | `[[a; 1]; [b; 2]]` or `[a; 1]; [b; 2]` |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Semicolons `;` are used as separators instead of commas `,`
|
||||||
|
- Outermost brackets `[]` are optional for tuple and array values
|
||||||
|
- Special characters can be escaped with backslash: `\;`, `\[`, `\]`, `\\`
|
||||||
|
- Empty arrays/tuples are not allowed
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "inline-schema",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||||
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
||||||
|
"test": "tsx src/test.ts"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"schema",
|
||||||
|
"parser",
|
||||||
|
"validator",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "A TypeScript library for parsing and validating inline schemas",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { parseSchema } from './parser';
|
||||||
|
import { parseValue, createValidator } from './validator';
|
||||||
|
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ParsedSchema } from './types';
|
||||||
|
import { ParseError } from './parser';
|
||||||
|
|
||||||
|
export function defineSchema(schemaString: string): ParsedSchema {
|
||||||
|
const schema = parseSchema(schemaString);
|
||||||
|
const validator = createValidator(schema);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
validator,
|
||||||
|
parse: (valueString: string) => parseValue(schema, valueString),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { parseSchema, parseValue, createValidator, ParseError };
|
||||||
|
export type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ParsedSchema };
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema } from './types';
|
||||||
|
|
||||||
|
export class ParseError extends Error {
|
||||||
|
constructor(message: string, public position?: number) {
|
||||||
|
super(position !== undefined ? `${message} at position ${position}` : message);
|
||||||
|
this.name = 'ParseError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
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 match(str: string): boolean {
|
||||||
|
return this.input.slice(this.pos, this.pos + str.length) === str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeStr(str: string): boolean {
|
||||||
|
if (this.match(str)) {
|
||||||
|
this.pos += str.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(): number {
|
||||||
|
return this.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputLength(): number {
|
||||||
|
return this.input.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSchema(): Schema {
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (this.consumeStr('string')) {
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
return { type: 'array', element: { type: 'string' } };
|
||||||
|
}
|
||||||
|
return { type: 'string' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.consumeStr('number')) {
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
return { type: 'array', element: { type: 'number' } };
|
||||||
|
}
|
||||||
|
return { type: 'number' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.consumeStr('boolean')) {
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
return { type: 'array', element: { type: 'boolean' } };
|
||||||
|
}
|
||||||
|
return { type: 'boolean' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
const elements: Schema[] = [];
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (this.peek() === ']') {
|
||||||
|
this.consume();
|
||||||
|
throw new ParseError('Empty array/tuple not allowed', this.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push(this.parseSchema());
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (this.consumeStr(';')) {
|
||||||
|
const remainingElements: Schema[] = [];
|
||||||
|
while (true) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
remainingElements.push(this.parseSchema());
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (!this.consumeStr(';')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elements.push(...remainingElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
if (elements.length === 1) {
|
||||||
|
return { type: 'array', element: elements[0] };
|
||||||
|
}
|
||||||
|
return { type: 'array', element: { type: 'tuple', elements } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.length === 1) {
|
||||||
|
return { type: 'array', element: elements[0] };
|
||||||
|
}
|
||||||
|
return { type: 'tuple', elements };
|
||||||
|
}
|
||||||
|
|
||||||
|
let identifier = '';
|
||||||
|
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
||||||
|
identifier += this.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifier.length > 0) {
|
||||||
|
if (this.consumeStr('[')) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
if (!this.consumeStr(']')) {
|
||||||
|
throw new ParseError('Expected ]', this.pos);
|
||||||
|
}
|
||||||
|
return { type: 'array', element: { type: 'string' } };
|
||||||
|
}
|
||||||
|
return { type: 'string' };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSchema(schemaString: string): Schema {
|
||||||
|
const parser = new Parser(schemaString.trim());
|
||||||
|
const schema = parser.parseSchema();
|
||||||
|
|
||||||
|
if (parser.getPosition() < parser.getInputLength()) {
|
||||||
|
throw new ParseError('Unexpected input after schema', parser.getPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { defineSchema, parseSchema, parseValue, createValidator } from './index';
|
||||||
|
|
||||||
|
console.log('=== Testing Schema Parser ===\n');
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ schema: 'string', value: 'hello', description: 'Simple string' },
|
||||||
|
{ schema: 'number', value: '42', description: 'Simple number' },
|
||||||
|
{ schema: 'boolean', value: 'true', description: 'Simple boolean' },
|
||||||
|
{ schema: '[string; number]', value: '[hello; 42]', description: 'Tuple' },
|
||||||
|
{ schema: '[string; number]', value: 'hello; 42', description: 'Tuple without brackets' },
|
||||||
|
{ schema: 'string[]', value: '[hello; world; test]', description: 'Array of strings' },
|
||||||
|
{ schema: 'string[]', value: 'hello; world; test', description: 'Array without brackets' },
|
||||||
|
{ schema: 'number[]', value: '[1; 2; 3; 4]', description: 'Array of numbers' },
|
||||||
|
{ schema: '[string; number][]', value: '[[a; 1]; [b; 2]; [c; 3]]', description: 'Array of tuples' },
|
||||||
|
{ schema: '[string; number][]', value: '[a; 1]; [b; 2]; [c; 3]', description: 'Array of tuples without outer brackets' },
|
||||||
|
{ schema: 'word-smith', value: 'word-smith', description: 'String with hyphen' },
|
||||||
|
{ schema: 'string', value: 'hello\\;world', description: 'Escaped semicolon' },
|
||||||
|
{ schema: 'string', value: 'hello\\[world', description: 'Escaped bracket' },
|
||||||
|
{ schema: 'string', value: 'hello\\\\world', description: 'Escaped backslash' },
|
||||||
|
{ schema: '[string; string]', value: 'hello\\;world; test', description: 'Tuple with escaped semicolon' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ schema, value, description }) => {
|
||||||
|
try {
|
||||||
|
console.log(`Test: ${description}`);
|
||||||
|
console.log(` Schema: ${schema}`);
|
||||||
|
console.log(` Value: "${value}"`);
|
||||||
|
|
||||||
|
const parsed = defineSchema(schema);
|
||||||
|
const parsedValue = parsed.parse(value);
|
||||||
|
const isValid = parsed.validator(parsedValue);
|
||||||
|
|
||||||
|
console.log(` Parsed: ${JSON.stringify(parsedValue)}`);
|
||||||
|
console.log(` Valid: ${isValid}`);
|
||||||
|
console.log(' ✓ Passed\n');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ Failed: ${(error as Error).message}\n`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Testing Validation ===\n');
|
||||||
|
|
||||||
|
const stringSchema = defineSchema('string');
|
||||||
|
console.log('String schema validation:');
|
||||||
|
console.log(` "hello" is valid: ${stringSchema.validator('hello')}`);
|
||||||
|
console.log(` 42 is valid: ${stringSchema.validator(42)}\n`);
|
||||||
|
|
||||||
|
const numberSchema = defineSchema('number');
|
||||||
|
console.log('Number schema validation:');
|
||||||
|
console.log(` 42 is valid: ${numberSchema.validator(42)}`);
|
||||||
|
console.log(` "42" is valid: ${numberSchema.validator('42')}\n`);
|
||||||
|
|
||||||
|
const tupleSchema = defineSchema('[string; number; boolean]');
|
||||||
|
console.log('Tuple [string; number; boolean] validation:');
|
||||||
|
console.log(` ["hello", 42, true] is valid: ${tupleSchema.validator(['hello', 42, true])}`);
|
||||||
|
console.log(` ["hello", "42", true] is valid: ${tupleSchema.validator(['hello', '42', true])}\n`);
|
||||||
|
|
||||||
|
const arraySchema = defineSchema('number[]');
|
||||||
|
console.log('Array number[] validation:');
|
||||||
|
console.log(` [1, 2, 3] is valid: ${arraySchema.validator([1, 2, 3])}`);
|
||||||
|
console.log(` [1, "2", 3] is valid: ${arraySchema.validator([1, '2', 3])}\n`);
|
||||||
|
|
||||||
|
const arrayOfTuplesSchema = defineSchema('[string; number][]');
|
||||||
|
console.log('Array of tuples [string; number][] validation:');
|
||||||
|
console.log(` [["a", 1], ["b", 2]] is valid: ${arrayOfTuplesSchema.validator([['a', 1], ['b', 2]])}`);
|
||||||
|
console.log(` [["a", "1"], ["b", 2]] is valid: ${arrayOfTuplesSchema.validator([['a', '1'], ['b', 2]])}\n`);
|
||||||
|
|
||||||
|
console.log('=== All tests completed ===');
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export type SchemaType = 'string' | 'number' | 'boolean';
|
||||||
|
|
||||||
|
export interface PrimitiveSchema {
|
||||||
|
type: SchemaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TupleSchema {
|
||||||
|
type: 'tuple';
|
||||||
|
elements: Schema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArraySchema {
|
||||||
|
type: 'array';
|
||||||
|
element: Schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Schema = PrimitiveSchema | TupleSchema | ArraySchema;
|
||||||
|
|
||||||
|
export interface ParsedSchema {
|
||||||
|
schema: Schema;
|
||||||
|
validator: (value: unknown) => boolean;
|
||||||
|
parse: (valueString: string) => unknown;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"types": ["node"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue