| // Copyright (C) 2021 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Execution context of object validator |
| interface ValidatorContext { |
| // Path to the current value starting from the root. Object field names are |
| // stored as is, array indices are wrapped to square brackets. Represented |
| // as an array to avoid unnecessary string concatenation: parts are going to |
| // be concatenated into a single string when reporting errors, which should |
| // not happen on a happy path. |
| // Example: ["config", "androidLogBuffers", "1"] when parsing object |
| // accessible through expression `root.config.androidLogBuffers[1]` |
| path: string[]; |
| |
| // Paths from the root to extraneous keys in a validated object. |
| extraKeys: string[]; |
| |
| // Paths from the root to keys containing values of wrong type in validated |
| // object. |
| invalidKeys: string[]; |
| } |
| |
| // Validator accepting arbitrary data structure and returning a typed value. |
| // Can throw an error if a part of the value does not have a reasonable |
| // default. |
| export interface Validator<T> { |
| validate(input: unknown, context: ValidatorContext): T; |
| } |
| |
| // Helper function to flatten array of path chunks into a single string |
| // Example: ["config", "androidLogBuffers", "1"] is mapped to |
| // "config.androidLogBuffers[1]". |
| function renderPath(path: string[]): string { |
| let result = ''; |
| for (let i = 0; i < path.length; i++) { |
| if (i > 0 && !path[i].startsWith('[')) { |
| result += '.'; |
| } |
| result += path[i]; |
| } |
| return result; |
| } |
| |
| export class ValidationError extends Error {} |
| |
| // Abstract class for validating simple values, such as strings and booleans. |
| // Allows to avoid repetition of most of the code related to validation of |
| // these. |
| abstract class PrimitiveValidator<T> implements Validator<T> { |
| defaultValue: T; |
| required: boolean; |
| |
| constructor(defaultValue: T, required: boolean) { |
| this.defaultValue = defaultValue; |
| this.required = required; |
| } |
| |
| // Abstract method that checks whether passed input has correct type. |
| abstract predicate(input: unknown): input is T; |
| |
| validate(input: unknown, context: ValidatorContext): T { |
| if (this.predicate(input)) { |
| return input; |
| } |
| if (this.required) { |
| throw new ValidationError(renderPath(context.path)); |
| } |
| if (input !== undefined) { |
| // The value is defined, but does not conform to the expected type; |
| // proceed with returning the default value but report the key. |
| context.invalidKeys.push(renderPath(context.path)); |
| } |
| return this.defaultValue; |
| } |
| } |
| |
| class OptionalValidator<T> implements Validator<T|undefined> { |
| private inner: Validator<T>; |
| |
| constructor(inner: Validator<T>) { |
| this.inner = inner; |
| } |
| |
| validate(input: unknown, context: ValidatorContext): T|undefined { |
| if (input === undefined) { |
| return undefined; |
| } |
| try { |
| return this.inner.validate(input, context); |
| } catch (e) { |
| if (e instanceof ValidationError) { |
| context.invalidKeys.push(renderPath(context.path)); |
| return undefined; |
| } |
| throw e; |
| } |
| } |
| } |
| |
| class StringValidator extends PrimitiveValidator<string> { |
| predicate(input: unknown): input is string { |
| return typeof input === 'string'; |
| } |
| } |
| |
| class NumberValidator extends PrimitiveValidator<number> { |
| predicate(input: unknown): input is number { |
| return typeof input === 'number'; |
| } |
| } |
| |
| class BooleanValidator extends PrimitiveValidator<boolean> { |
| predicate(input: unknown): input is boolean { |
| return typeof input === 'boolean'; |
| } |
| } |
| |
| // Type-level function returning resulting type of a validator. |
| export type ValidatedType<T> = T extends Validator<infer S>? S : never; |
| |
| // Type-level function traversing a record of validator and returning record |
| // with the same keys and valid types. |
| export type RecordValidatedType<T> = { |
| [k in keyof T]: ValidatedType<T[k]> |
| }; |
| |
| // Combinator for validators: takes a record of validators, and returns a |
| // validator for a record where record's fields passed to validator with the |
| // same name. |
| // |
| // Generic parameter T is instantiated to type of record of validators, and |
| // should be provided implicitly by type inference due to verbosity of its |
| // instantiations. |
| class RecordValidator<T extends Record<string, Validator<unknown>>> implements |
| Validator<RecordValidatedType<T>> { |
| validators: T; |
| |
| constructor(validators: T) { |
| this.validators = validators; |
| } |
| |
| validate(input: unknown, context: ValidatorContext): RecordValidatedType<T> { |
| // If value is missing or of incorrect type, empty record is still processed |
| // in the loop below to initialize default fields of the nested object. |
| let o: object = {}; |
| if (typeof input === 'object' && input !== null) { |
| o = input; |
| } else if (input !== undefined) { |
| context.invalidKeys.push(renderPath(context.path)); |
| } |
| |
| const result: Partial<RecordValidatedType<T>> = {}; |
| // Separate declaration is required to avoid assigning `string` type to `k`. |
| for (const k in this.validators) { |
| if (this.validators.hasOwnProperty(k)) { |
| context.path.push(k); |
| const validator = this.validators[k]; |
| |
| // Accessing value of `k` of `o` is safe because `undefined` values are |
| // considered to indicate a missing value and handled appropriately by |
| // every provided validator. |
| const valid = |
| validator.validate((o as Record<string, unknown>)[k], context); |
| |
| result[k] = valid as ValidatedType<T[string]>; |
| context.path.pop(); |
| } |
| } |
| |
| // Check if passed object has any extra keys to be reported as such. |
| for (const key of Object.keys(o)) { |
| if (!this.validators.hasOwnProperty(key)) { |
| context.path.push(key); |
| context.extraKeys.push(renderPath(context.path)); |
| context.path.pop(); |
| } |
| } |
| return result as RecordValidatedType<T>; |
| } |
| } |
| |
| // Validator checking whether a value is one of preset values. Used in order to |
| // provide easy validation for union of literal types. |
| class OneOfValidator<T> implements Validator<T> { |
| validValues: readonly T[]; |
| defaultValue: T; |
| |
| constructor(validValues: readonly T[], defaultValue: T) { |
| this.defaultValue = defaultValue; |
| this.validValues = validValues; |
| } |
| |
| validate(input: unknown, context: ValidatorContext): T { |
| if (this.validValues.includes(input as T)) { |
| return input as T; |
| } else if (input !== undefined) { |
| context.invalidKeys.push(renderPath(context.path)); |
| } |
| return this.defaultValue; |
| } |
| } |
| |
| // Validator for an array of elements, applying the same element validator for |
| // each element of an array. Uses empty array as a default value. |
| class ArrayValidator<T> implements Validator<T[]> { |
| elementValidator: Validator<T>; |
| |
| constructor(elementValidator: Validator<T>) { |
| this.elementValidator = elementValidator; |
| } |
| |
| validate(input: unknown, context: ValidatorContext): T[] { |
| const result: T[] = []; |
| if (Array.isArray(input)) { |
| for (let i = 0; i < input.length; i++) { |
| context.path.push(`[${i}]`); |
| result.push(this.elementValidator.validate(input[i], context)); |
| context.path.pop(); |
| } |
| } else if (input !== undefined) { |
| context.invalidKeys.push(renderPath(context.path)); |
| } |
| return result; |
| } |
| } |
| |
| // Wrapper container for validation result contaiting diagnostic information in |
| // addition to the resulting typed value. |
| export interface ValidationResult<T> { |
| result: T; |
| invalidKeys: string[]; |
| extraKeys: string[]; |
| } |
| |
| // Wrapper for running a validator initializing the context. |
| export function runValidator<T>( |
| validator: Validator<T>, input: unknown): ValidationResult<T> { |
| const context: ValidatorContext = { |
| path: [], |
| invalidKeys: [], |
| extraKeys: [], |
| }; |
| const result = validator.validate(input, context); |
| return { |
| result, |
| invalidKeys: context.invalidKeys, |
| extraKeys: context.extraKeys, |
| }; |
| } |
| |
| // Shorthands for the validator classes above enabling concise notation. |
| export function str(defaultValue = ''): StringValidator { |
| return new StringValidator(defaultValue, false); |
| } |
| |
| export const requiredStr = new StringValidator('', true); |
| |
| export const optStr = new OptionalValidator<string>(requiredStr); |
| |
| export function num(defaultValue = 0): NumberValidator { |
| return new NumberValidator(defaultValue, false); |
| } |
| |
| export const requiredNum = new NumberValidator(0, true); |
| |
| export const optNum = new OptionalValidator<number>(requiredNum); |
| |
| export function bool(defaultValue = false): BooleanValidator { |
| return new BooleanValidator(defaultValue, false); |
| } |
| |
| export const requiredBool = new BooleanValidator(false, true); |
| |
| export const optBool = new OptionalValidator<boolean>(requiredBool); |
| |
| |
| export function record<T extends Record<string, Validator<unknown>>>( |
| validators: T): RecordValidator<T> { |
| return new RecordValidator<T>(validators); |
| } |
| |
| export function oneOf<T>( |
| values: readonly T[], defaultValue: T): OneOfValidator<T> { |
| return new OneOfValidator<T>(values, defaultValue); |
| } |
| |
| export function arrayOf<T>(elementValidator: Validator<T>): ArrayValidator<T> { |
| return new ArrayValidator<T>(elementValidator); |
| } |