Path: blob/main/extensions/copilot/src/platform/configuration/common/validator.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { JsonSchema } from './jsonSchema';67export interface IValidator<T> {8validate(content: unknown): { content: T; error: undefined } | { content: undefined; error: ValidationError };910toSchema(): JsonSchema;1112/**13* Returns true if this validator represents a required field.14* Used by vObj to determine which fields are required.15*/16isRequired?(): boolean;17}1819export type ValidatorType<T> = T extends IValidator<infer U> ? U : never;2021export interface ValidationError {22message: string;23}2425interface TypeOfMap {26string: string;27number: number;28boolean: boolean;29object: object;30undefined: undefined;31function: Function;32symbol: symbol;33}3435class TypeofValidator<TKey extends keyof TypeOfMap> implements IValidator<TypeOfMap[TKey]> {36constructor(private readonly type: TKey) { }3738validate(content: unknown): { content: TypeOfMap[TKey]; error: undefined } | { content: undefined; error: ValidationError } {39if (typeof content !== this.type) {40return { content: undefined, error: { message: `Expected ${this.type}, but got ${typeof content}` } };41}4243return { content: content as TypeOfMap[TKey], error: undefined };44}4546toSchema(): JsonSchema {47return { type: this.type };48}49}5051const vStringValidator = new TypeofValidator('string');52export function vString(): IValidator<string> { return vStringValidator; }5354const vNumberValidator = new TypeofValidator('number');55export function vNumber(): IValidator<number> { return vNumberValidator; }5657const vBooleanValidator = new TypeofValidator('boolean');58export function vBoolean(): IValidator<boolean> { return vBooleanValidator; }5960const vObjAnyValidator = new TypeofValidator('object');61export function vObjAny(): IValidator<object> { return vObjAnyValidator; }6263const vUndefinedValidator = new TypeofValidator('undefined');64export function vUndefined(): IValidator<undefined> { return vUndefinedValidator; }6566class NullValidator implements IValidator<null> {67validate(content: unknown): { content: null; error: undefined } | { content: undefined; error: ValidationError } {68if (content !== null) {69return { content: undefined, error: { message: `Expected null, but got ${typeof content}` } };70}71return { content: null, error: undefined };72}7374toSchema(): JsonSchema {75return { type: 'null' };76}77}7879const vNullValidator = new NullValidator();80export function vNull(): IValidator<null> { return vNullValidator; }8182export function vNullable<T>(validator: IValidator<T>): IValidator<T | null> {83return vUnion(validator, vNullValidator);84}8586export function vUnchecked<T>(): IValidator<T> {87return {88validate(content: unknown): { content: T; error: undefined } {89return { content: content as T, error: undefined };90},91toSchema() {92return {9394};95},96};97}9899export function vUnknown(): IValidator<unknown> {100return vUnchecked();101}102103export type ObjectProperties = Record<string, any>;104105export function vRequired<T>(validator: IValidator<T>): IValidator<T> {106return {107validate(content: unknown): { content: T; error: undefined } | { content: undefined; error: ValidationError } {108if (content === undefined) {109return { content: undefined, error: { message: 'Required field is missing' } };110}111return validator.validate(content);112},113toSchema(): JsonSchema {114return validator.toSchema();115},116isRequired(): boolean {117return true;118}119};120}121122export function vObj<T extends Record<string, IValidator<any>>>(properties: T): IValidator<{ [K in keyof T]: ValidatorType<T[K]> }> {123return {124validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {125if (typeof content !== 'object' || content === null) {126return { content: undefined, error: { message: 'Expected object' } };127}128129const result: any = {};130for (const key in properties) {131const validator = properties[key];132const fieldValue = (content as any)[key];133134// Check if field is required and missing135const isRequired = validator.isRequired?.() ?? false;136if (isRequired && fieldValue === undefined) {137return { content: undefined, error: { message: `Required field '${key}' is missing` } };138}139140// If field is not required and is missing, skip validation141if (!isRequired && fieldValue === undefined) {142continue;143}144145const { content: value, error } = validator.validate(fieldValue);146if (error) {147return { content: undefined, error: { message: `Error in property '${key}': ${error.message}` } };148}149150result[key] = value;151}152153return { content: result, error: undefined };154},155toSchema() {156const requiredFields: string[] = [];157const schemaProperties: Record<string, JsonSchema> = {};158159for (const [key, validator] of Object.entries(properties)) {160schemaProperties[key] = validator.toSchema();161if (validator.isRequired?.()) {162requiredFields.push(key);163}164}165166const schema: JsonSchema = {167type: 'object',168properties: schemaProperties,169...(requiredFields.length > 0 ? { required: requiredFields } : {})170};171172return schema;173}174};175}176177export function vArray<T>(validator: IValidator<T>): IValidator<T[]> {178return {179validate(content: unknown): { content: T[]; error: undefined } | { content: undefined; error: ValidationError } {180if (!Array.isArray(content)) {181return { content: undefined, error: { message: 'Expected array' } };182}183184const result: T[] = [];185for (let i = 0; i < content.length; i++) {186const { content: value, error } = validator.validate(content[i]);187if (error) {188return { content: undefined, error: { message: `Error in element ${i}: ${error.message}` } };189}190191result.push(value);192}193194return { content: result, error: undefined };195},196197toSchema(): JsonSchema {198return {199type: 'array',200items: validator.toSchema(),201};202}203};204}205206export function vTuple<T extends IValidator<any>[]>(...validators: T): IValidator<{ [K in keyof T]: ValidatorType<T[K]> }> {207return {208validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {209if (!Array.isArray(content)) {210return { content: undefined, error: { message: 'Expected array' } };211}212213if (content.length !== validators.length) {214return { content: undefined, error: { message: `Expected tuple of length ${validators.length}, but got ${content.length}` } };215}216217const result: any = [];218for (let i = 0; i < validators.length; i++) {219const validator = validators[i];220const { content: value, error } = validator.validate(content[i]);221if (error) {222return { content: undefined, error: { message: `Error in element ${i}: ${error.message}` } };223}224225result.push(value);226}227228return { content: result, error: undefined };229},230231toSchema(): JsonSchema {232return {233type: 'array',234items: validators.map(validator => validator.toSchema()),235};236}237};238}239240export function vUnion<T extends IValidator<any>[]>(...validators: T): IValidator<ValidatorType<T[number]>> {241return {242validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {243let lastError: ValidationError | undefined;244for (const validator of validators) {245const { content: value, error } = validator.validate(content);246if (!error) {247return { content: value, error: undefined };248}249250lastError = error;251}252253return { content: undefined, error: lastError! };254},255256toSchema(): JsonSchema {257return {258oneOf: validators.map(validator => validator.toSchema()),259};260}261};262}263264export function vEnum<T extends string[]>(...values: T): IValidator<T[number]> {265return {266validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {267if (values.indexOf(content as any) === -1) {268return { content: undefined, error: { message: `Expected one of: ${values.join(', ')}` } };269}270271return { content, error: undefined };272},273274toSchema(): JsonSchema {275return {276enum: values,277};278}279};280}281282export function vLiteral<T extends string>(value: T): IValidator<T> {283return {284validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {285if (content !== value) {286return { content: undefined, error: { message: `Expected: ${value}` } };287}288289return { content, error: undefined };290},291292toSchema(): JsonSchema {293return {294const: value,295};296}297};298}299300export function vLazy<T>(fn: () => IValidator<T>): IValidator<T> {301return {302validate(content: unknown): { content: any; error: undefined } | { content: undefined; error: ValidationError } {303return fn().validate(content);304},305306toSchema(): JsonSchema {307return fn().toSchema();308}309};310}311312313