1import { CommandError, UnimplementedError } from '../../utils/errors';
2import { memoize } from '../../utils/fn';
3
4/** An error that is memoized and asserted whenever a Prerequisite.assertAsync is subsequently called. */
5export class PrerequisiteCommandError extends CommandError {
6  constructor(code: string, message: string = '') {
7    super(message ? 'VALIDATE_' + code : code, message);
8  }
9}
10
11export class Prerequisite<T = void, TProps = void> {
12  /** Memoized results of `assertImplementation` */
13  private _assertAsync: (props: TProps) => Promise<T>;
14
15  constructor() {
16    this._assertAsync = memoize(this.assertImplementation.bind(this));
17  }
18
19  /** An optional warning to call before running the memoized assertion.  */
20  protected cachedError?: PrerequisiteCommandError;
21
22  /** Reset the assertion memo and warning message. */
23  public resetAssertion(props: TProps) {
24    this.cachedError = undefined;
25    this._assertAsync = memoize(this.assertImplementation.bind(this));
26  }
27
28  async assertAsync(props: TProps): Promise<T> {
29    if (this.cachedError) {
30      throw this.cachedError;
31    }
32    try {
33      return await this._assertAsync(props);
34    } catch (error) {
35      if (error instanceof PrerequisiteCommandError) {
36        this.cachedError = error;
37      }
38      throw error;
39    }
40  }
41
42  /** Exposed for testing. */
43  async assertImplementation(props: TProps): Promise<T> {
44    throw new UnimplementedError();
45  }
46}
47
48/** A prerequisite that is project specific. */
49export class ProjectPrerequisite<T = void, TProps = void> extends Prerequisite<T, TProps> {
50  constructor(protected projectRoot: string) {
51    super();
52  }
53}
54