1
0
Fork 0
tsgames/src/common/typebox.ts

261 lines
8.8 KiB
TypeScript

const optional = Symbol();
const GlobalObject = Object;
const GlobalNumber = Number;
const GlobalArray = Array;
export namespace Type {
export function String<S extends string = string>(args: { description?: string, enum?: S[] } = {}) {
const result: TString<S> = {
type: 'string',
};
if (args.enum) {
result.enum = args.enum;
}
if (args.description) {
result.description = args.description;
}
return result;
}
export function Number<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
const result: TNumber<N> = {
type: 'number',
};
if (args.enum) {
result.enum = args.enum;
}
if (args.description) {
result.description = args.description;
}
return result;
}
export function Integer<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
const result: TNumber<N> = {
type: 'integer',
};
if (args.enum) {
result.enum = args.enum;
}
if (args.description) {
result.description = args.description;
}
return result;
}
export function Boolean(args: { description?: string } = {}) {
const result: TBoolean = {
type: 'boolean',
};
if (args.description) {
result.description = args.description;
}
return result;
}
export function Array<T extends TScheme = TScheme>(items: T, args: { description?: string } = {}) {
const result: TArray<T> = {
type: 'array',
items,
}
if (args.description) {
result.description = args.description;
}
return result;
}
export function Optional<T extends TScheme = TScheme>(scheme: T): TOptional<T> {
const result = { ...scheme };
GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false });
return result as unknown as TOptional<T>;
}
export function Object<T extends TProperties = TProperties>(properties: T, args: { description?: string } = {}) {
const result: TObject<T> = {
type: 'object',
properties,
required: GlobalObject.keys(properties)
.filter(key => !(properties[key] as any)[optional]),
}
if (args.description) {
result.description = args.description;
}
return result;
}
export function Enum<S extends string, T extends TString<S>>(items: S[], args?: { description?: string }): T;
export function Enum<N extends number, T extends TNumber<N>>(items: N[], args?: { description?: string }): T;
export function Enum(items: (number | string)[], args: { description?: string } = {}) {
if (typeof items?.[0] === 'number') {
return Number({ enum: items.map(item => +item!), ...args });
}
return String({ enum: items.map(item => item!.toString()), ...args });
}
export interface CheckError {
path: string;
message: string;
}
function check(scheme: TScheme, value: unknown, path: string): CheckError[] {
if (value == null) {
if ((scheme as any)[optional]) return [];
return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }];
}
switch (scheme.type) {
case 'string': {
if (typeof value !== 'string') {
return [{ path, message: `Expected string at ${path}, got ${typeof value}` }];
}
if (scheme.enum && !scheme.enum.includes(value)) {
return [{ path, message: `Expected one of [${scheme.enum.join(', ')}] at ${path}, got "${value}"` }];
}
return [];
}
case 'number':
case 'integer': {
if (typeof value !== 'number') {
return [{ path, message: `Expected number at ${path}, got ${typeof value}` }];
}
if (scheme.type === 'integer' && !GlobalNumber.isInteger(value)) {
return [{ path, message: `Expected integer at ${path}, got ${value}` }];
}
const s = scheme as TNumber;
if (s.enum && !s.enum.includes(value)) {
return [{ path, message: `Expected one of [${s.enum.join(', ')}] at ${path}, got ${value}` }];
}
return [];
}
case 'boolean': {
if (typeof value !== 'boolean') {
return [{ path, message: `Expected boolean at ${path}, got ${typeof value}` }];
}
return [];
}
case 'array': {
if (!GlobalArray.isArray(value)) {
return [{ path, message: `Expected array at ${path}, got ${typeof value}` }];
}
const s = scheme as TArray;
return value.flatMap((item, i) => check(s.items, item, `${path}[${i}]`));
}
case 'object': {
if (typeof value !== 'object' || GlobalArray.isArray(value)) {
return [{ path, message: `Expected object at ${path}, got ${typeof value}` }];
}
const s = scheme as TObject;
const errors: CheckError[] = [];
for (const key of s.required || []) {
if (!(key in (value as Record<string, unknown>))) {
errors.push({ path: `${path}.${key}`, message: `Missing required property ${path}.${key}` });
}
}
for (const [key, propScheme] of GlobalObject.entries(s.properties)) {
const propValue = (value as Record<string, unknown>)[key];
if (propValue === undefined) continue;
errors.push(...check(propScheme, propValue, `${path}.${key}`));
}
return errors;
}
default:
return [];
}
}
export function Check(scheme: TScheme, value: unknown): CheckError[] {
return check(scheme, value, '$');
}
export function Is<T extends TScheme = TScheme>(scheme: T, value: unknown): value is Static<T> {
return Check(scheme, value).length === 0;
}
}
export interface TString<T extends string = string> {
type: 'string';
enum?: T[];
description?: string;
}
export interface TNumber<T extends number = number> {
type: 'number' | 'integer';
enum?: T[];
description?: string;
}
export interface TBoolean {
type: 'boolean';
description?: string;
}
export interface TArray<T extends TScheme = TScheme> {
type: 'array';
description?: string;
items: T;
}
export type TProperties = Record<string, TScheme>;
export interface TObject<T extends TProperties = TProperties> {
type: 'object';
description?: string;
properties: T;
required?: string[];
}
export interface TOptionalString<T extends string = string> extends TString<T> {
[optional]: true;
}
export interface TOptionalNumber<T extends number = number> extends TNumber<T> {
[optional]: true;
}
export interface TOptionalBoolean extends TBoolean {
[optional]: true;
}
export interface TOptionalArray<T extends TScheme = TScheme> extends TArray<T> {
[optional]: true;
}
export interface TOptionalObject<T extends TProperties = TProperties> extends TObject<T> {
[optional]: true;
}
export type TOptional<T extends TScheme = TScheme> =
T extends TString<infer S> ? TOptionalString<S> :
T extends TNumber<infer N> ? TOptionalNumber<N> :
T extends TBoolean ? TOptionalBoolean :
T extends TArray<infer I> ? TOptionalArray<I> :
T extends TObject<infer P> ? TOptionalObject<P> :
never;
export type IsOptional<T> = T extends { [optional]: true } ? true : false;
export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject;
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type RequiredKeys<T extends TProperties> = {
[K in keyof T]: IsOptional<T[K]> extends true ? never : K
}[keyof T];
type OptionalKeys<T extends TProperties> = {
[K in keyof T]: IsOptional<T[K]> extends true ? K : never
}[keyof T];
type StaticObject<T extends TProperties> = Prettify<
{ [K in RequiredKeys<T>]: Static<T[K]> } &
{ [K in OptionalKeys<T>]?: Static<T[K]> }
>;
export type Static<T extends TScheme> =
T extends TString<infer S> ? S :
T extends TNumber<infer N> ? N :
T extends TBoolean ? boolean :
T extends TArray<infer I> ? Static<I>[] :
T extends TObject<infer P> ? StaticObject<P> :
never;