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