1// Copyright (C) 2021 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Execution context of object validator 16interface ValidatorContext { 17 // Path to the current value starting from the root. Object field names are 18 // stored as is, array indices are wrapped to square brackets. Represented 19 // as an array to avoid unnecessary string concatenation: parts are going to 20 // be concatenated into a single string when reporting errors, which should 21 // not happen on a happy path. 22 // Example: ["config", "androidLogBuffers", "1"] when parsing object 23 // accessible through expression `root.config.androidLogBuffers[1]` 24 path: string[]; 25 26 // Paths from the root to extraneous keys in a validated object. 27 extraKeys: string[]; 28 29 // Paths from the root to keys containing values of wrong type in validated 30 // object. 31 invalidKeys: string[]; 32} 33 34// Validator accepting arbitrary data structure and returning a typed value. 35// Can throw an error if a part of the value does not have a reasonable 36// default. 37export interface Validator<T> { 38 validate(input: unknown, context: ValidatorContext): T; 39} 40 41// Helper function to flatten array of path chunks into a single string 42// Example: ["config", "androidLogBuffers", "1"] is mapped to 43// "config.androidLogBuffers[1]". 44function renderPath(path: string[]): string { 45 let result = ''; 46 for (let i = 0; i < path.length; i++) { 47 if (i > 0 && !path[i].startsWith('[')) { 48 result += '.'; 49 } 50 result += path[i]; 51 } 52 return result; 53} 54 55export class ValidationError extends Error {} 56 57// Abstract class for validating simple values, such as strings and booleans. 58// Allows to avoid repetition of most of the code related to validation of 59// these. 60abstract class PrimitiveValidator<T> implements Validator<T> { 61 defaultValue: T; 62 required: boolean; 63 64 constructor(defaultValue: T, required: boolean) { 65 this.defaultValue = defaultValue; 66 this.required = required; 67 } 68 69 // Abstract method that checks whether passed input has correct type. 70 abstract predicate(input: unknown): input is T; 71 72 validate(input: unknown, context: ValidatorContext): T { 73 if (this.predicate(input)) { 74 return input; 75 } 76 if (this.required) { 77 throw new ValidationError(renderPath(context.path)); 78 } 79 if (input !== undefined) { 80 // The value is defined, but does not conform to the expected type; 81 // proceed with returning the default value but report the key. 82 context.invalidKeys.push(renderPath(context.path)); 83 } 84 return this.defaultValue; 85 } 86} 87 88 89class StringValidator extends PrimitiveValidator<string> { 90 predicate(input: unknown): input is string { 91 return typeof input === 'string'; 92 } 93} 94 95class NumberValidator extends PrimitiveValidator<number> { 96 predicate(input: unknown): input is number { 97 return typeof input === 'number'; 98 } 99} 100 101class BooleanValidator extends PrimitiveValidator<boolean> { 102 predicate(input: unknown): input is boolean { 103 return typeof input === 'boolean'; 104 } 105} 106 107// Type-level function returning resulting type of a validator. 108export type ValidatedType<T> = T extends Validator<infer S>? S : never; 109 110// Type-level function traversing a record of validator and returning record 111// with the same keys and valid types. 112export type RecordValidatedType<T> = { 113 [k in keyof T]: ValidatedType<T[k]> 114}; 115 116// Combinator for validators: takes a record of validators, and returns a 117// validator for a record where record's fields passed to validator with the 118// same name. 119// 120// Generic parameter T is instantiated to type of record of validators, and 121// should be provided implicitly by type inference due to verbosity of its 122// instantiations. 123class RecordValidator<T extends Record<string, Validator<unknown>>> implements 124 Validator<RecordValidatedType<T>> { 125 validators: T; 126 127 constructor(validators: T) { 128 this.validators = validators; 129 } 130 131 validate(input: unknown, context: ValidatorContext): RecordValidatedType<T> { 132 // If value is missing or of incorrect type, empty record is still processed 133 // in the loop below to initialize default fields of the nested object. 134 let o: object = {}; 135 if (typeof input === 'object' && input !== null) { 136 o = input; 137 } else if (input !== undefined) { 138 context.invalidKeys.push(renderPath(context.path)); 139 } 140 141 const result: Partial<RecordValidatedType<T>> = {}; 142 // Separate declaration is required to avoid assigning `string` type to `k`. 143 for (const k in this.validators) { 144 if (this.validators.hasOwnProperty(k)) { 145 context.path.push(k); 146 const validator = this.validators[k]; 147 148 // Accessing value of `k` of `o` is safe because `undefined` values are 149 // considered to indicate a missing value and handled appropriately by 150 // every provided validator. 151 const valid = 152 validator.validate((o as Record<string, unknown>)[k], context); 153 154 result[k] = valid as ValidatedType<T[string]>; 155 context.path.pop(); 156 } 157 } 158 159 // Check if passed object has any extra keys to be reported as such. 160 for (const key of Object.keys(o)) { 161 if (!this.validators.hasOwnProperty(key)) { 162 context.path.push(key); 163 context.extraKeys.push(renderPath(context.path)); 164 context.path.pop(); 165 } 166 } 167 return result as RecordValidatedType<T>; 168 } 169} 170 171// Validator checking whether a value is one of preset values. Used in order to 172// provide easy validation for union of literal types. 173class OneOfValidator<T> implements Validator<T> { 174 validValues: readonly T[]; 175 defaultValue: T; 176 177 constructor(validValues: readonly T[], defaultValue: T) { 178 this.defaultValue = defaultValue; 179 this.validValues = validValues; 180 } 181 182 validate(input: unknown, context: ValidatorContext): T { 183 if (this.validValues.includes(input as T)) { 184 return input as T; 185 } else if (input !== undefined) { 186 context.invalidKeys.push(renderPath(context.path)); 187 } 188 return this.defaultValue; 189 } 190} 191 192// Validator for an array of elements, applying the same element validator for 193// each element of an array. Uses empty array as a default value. 194class ArrayValidator<T> implements Validator<T[]> { 195 elementValidator: Validator<T>; 196 197 constructor(elementValidator: Validator<T>) { 198 this.elementValidator = elementValidator; 199 } 200 201 validate(input: unknown, context: ValidatorContext): T[] { 202 const result: T[] = []; 203 if (Array.isArray(input)) { 204 for (let i = 0; i < input.length; i++) { 205 context.path.push(`[${i}]`); 206 result.push(this.elementValidator.validate(input[i], context)); 207 context.path.pop(); 208 } 209 } else if (input !== undefined) { 210 context.invalidKeys.push(renderPath(context.path)); 211 } 212 return result; 213 } 214} 215 216// Wrapper container for validation result contaiting diagnostic information in 217// addition to the resulting typed value. 218export interface ValidationResult<T> { 219 result: T; 220 invalidKeys: string[]; 221 extraKeys: string[]; 222} 223 224// Wrapper for running a validator initializing the context. 225export function runValidator<T>( 226 validator: Validator<T>, input: unknown): ValidationResult<T> { 227 const context: ValidatorContext = { 228 path: [], 229 invalidKeys: [], 230 extraKeys: [], 231 }; 232 const result = validator.validate(input, context); 233 return { 234 result, 235 invalidKeys: context.invalidKeys, 236 extraKeys: context.extraKeys, 237 }; 238} 239 240// Shorthands for the validator classes above enabling concise notation. 241export function str(defaultValue = ''): StringValidator { 242 return new StringValidator(defaultValue, false); 243} 244 245export const requiredStr = new StringValidator('', true); 246 247export function num(defaultValue = 0): NumberValidator { 248 return new NumberValidator(defaultValue, false); 249} 250 251export function bool(defaultValue = false): BooleanValidator { 252 return new BooleanValidator(defaultValue, false); 253} 254 255export function record<T extends Record<string, Validator<unknown>>>( 256 validators: T): RecordValidator<T> { 257 return new RecordValidator<T>(validators); 258} 259 260export function oneOf<T>( 261 values: readonly T[], defaultValue: T): OneOfValidator<T> { 262 return new OneOfValidator<T>(values, defaultValue); 263} 264 265export function arrayOf<T>(elementValidator: Validator<T>): ArrayValidator<T> { 266 return new ArrayValidator<T>(elementValidator); 267} 268