• 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
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