• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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