All files / src/api RatifiedContract.ts

96.15% Statements 25/26
87.5% Branches 7/8
100% Functions 8/8
96.15% Lines 25/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 10037x   37x 37x                     37x 239x               37x 545x                 37x 301x                                     239x       545x 2x   543x 543x 543x   8x                             238x           240x 238x 238x       240x 238x   2x     37x 238x  
import { BasicContract } from "@jonloucks/contracts-ts/api/BasicContract";
import { Config, Contract } from "@jonloucks/contracts-ts/api/Contract";
import { ContractException } from "@jonloucks/contracts-ts/api/ContractException";
import { OptionalType, RequiredType, isNotPresent, isPresent } from "@jonloucks/contracts-ts/api/Types";
 
/**
 * A RatifiedContract is a Contract that has been verified to have either a test or cast function.
 * This ensures that the contract can actually perform some form of validation or transformation.
 * A type guard is required to create a RatifiedContract.
 *
 * @param <T> the type of deliverable for this Contract
 * @param config the configuration for the RatifiedContract
 * @returns the created RatifiedContract
 */
export function create<T>(config?: Config<T> | undefined): Contract<T> {
  return RatifiedContract.create<T>(config);
}
 
/**
 * Checks if the given instance is a RatifiedContract.
 * @param instance the instance to check
 * @returns true if the instance is a RatifiedContract, false otherwise
 */
export function isRatifiedContract(instance: unknown): instance is RatifiedContract<unknown> {
  return RatifiedContract.isRatifiedContract(instance);
}
 
/**
 * Checks if the given configuration is ratifiable.
 * 
 * @param config the configuration to check
 * @returns true if the configuration is ratifiable, false otherwise
 */
export function isRatifiableConfig<T>(config?: OptionalType<Config<T>>): config is RequiredType<Config<T>> {
  return isPresent(config) && isPresent(config.test);
}
 
/**
 * A RatifiedContract is a Contract that has been verified to have either a test or cast function.
 * This ensures that the contract can actually perform some form of validation or transformation.
 *
 * @param <T> the type of deliverable for this Contract
 */
class RatifiedContract<T> extends BasicContract<T> {
 
  /**
    * Create a contract derived from the given configuration
    *
    * @param config the name for the contract, null is not allowed
    * @param <T>    the type of deliverable for this Contract
    * @return the new Contract
    */
  static create<T>(config?: Config<T> | undefined): Contract<T> {
    return new RatifiedContract<T>(config);
  }
 
  static isRatifiedContract(instance: unknown): instance is RatifiedContract<unknown> {
    if (isNotPresent(instance)) {
      return false;
    }
    try {
      const candidate = instance as RatifiedContract<unknown>;
      return candidate.#secret === RatifiedContract.#SECRET;
    } catch {
      return false;
    }
  }
 
  /**
   * Being a RatifiedContract means something special. It is not something that you proclaim
   * by extending the class or duck-typing.
   * This is an integrity check to prevent duck-typing or extending Contract class.
   * Since private constructors can still be invoked.
   * This is not a security mechanism, just an integrity check.
   * This relies on TypeScript private fields which are enforced at runtime.
   * 
   * @throws ContractException when integrity check fails
   */
  private integrityCheck(): void {
    Iif (this.#secret !== RatifiedContract.#SECRET) {
      throw new ContractException('Integrity violation detected. This is not permitted.');
    }
  }
 
  private constructor(config?: OptionalType<Config<T>>) {
    super(RatifiedContract.validateConfig(config));
    Object.freeze(this);
    this.integrityCheck();
  }
 
  private static validateConfig<T>(config?: OptionalType<Config<T>>): RequiredType<Config<T>> {
    if (isRatifiableConfig(config)) {
      return config as RequiredType<Config<T>>;
    }
    throw new ContractException("RatifiedContract requires either a test or cast function must be present.");
  }
 
  static readonly #SECRET: symbol = Symbol("Contract");
  readonly #secret: symbol = RatifiedContract.#SECRET;
}