Skip to content

Latest commit

 

History

History
151 lines (121 loc) · 5.21 KB

File metadata and controls

151 lines (121 loc) · 5.21 KB

ContractCase Maintainer Documentation: How to add a new matcher

This describes the process to add in a new matcher for maintainers. Adding a new matcher as a plugin will be a subset of these steps.

Creating the types

First create the types in packages/case-entities. These describe the raw data (mostly parameters if your matcher has any)

  1. All matchers much have a constant for the type of the matcher. The type must have an exported constant for its type. This is used to determine what type of matcher it is and to run the associated matching functions. For example:

    export const ARRAY_LENGTH_MATCHER_TYPE = 'ArrayLength' as const;
  2. Export a new interface that describes the actual matcher JSON. This is what will be written to the contract file, and generated by the matcher DSL.

    It must include case:matcher:type, set to the exact type constant string you created in the previous step. All parameter fields must be prefixed with case:matcher:. For example:

    export interface CoreArrayLengthMatcher {
      'case:matcher:type': typeof ARRAY_LENGTH_MATCHER_TYPE;
      'case:matcher:minLength': number;
      'case:matcher:maxLength': number;
    }

    If your matcher modifies the context, add fields prefixed with case:context: - these are automatically picked up by ContractCase and rolled into the context before this matcher is invoked (and passed down to any child matchers).

  3. Add these new types to both AnyCaseNodeType and AnyCaseMatcher:

    export type AnyCaseNodeType =
      // ...etc
      typeof ARRAY_LENGTH_MATCHER_TYPE;
    
    export type AnyCaseMatcher =
      // ...etc
      CoreArrayLengthMatcher;

Creating the MatcherExecutor

Next, we will add the behaviour of the matcher, both for matching, and for stripping the matchers. This goes in packages/case-core

  1. Add a new MatcherExecutor<typeof YOUR_NEW_TYPE> in an appropriate place in diffmatch. For example:

    const strip: StripMatcherFn<typeof ARRAY_LENGTH_MATCHER_TYPE> = (
        matcher: CoreArrayLengthMatcher,
        matchContext: MatchContext
    ): AnyData => // implement the strip matcher function here
    
    
    const check: CheckMatchFn<typeof ARRAY_LENGTH_MATCHER_TYPE> = (
        matcher: CoreArrayLengthMatcher,
        matchContext: MatchContext,
        actual: unknown
    ): Promise<MatchResult> | MatchResult => // Implement your check here
    
    export const ArrayLengthExecutor: MatcherExecutor<
        typeof ARRAY_LENGTH_MATCHER_TYPE
    > = { check, strip };

    If you need to recurse further into any children of your matchers, use matchContext.descendAndCheck() or matchContext.descendAndStrip() as appropriate. See the existing matchers for examples.

    If your matcher doesn't have enough context to strip matchers (eg, for auxillery matchers designed to be used with and()), then throw a new StripUnsupportedError(matcher, matchContext) inside your implementation of strip().

    Note that matcher executors are not allowed to call other matcher executors - only descendAndCheck(). If you need to combine matchers, do it at the DSL layer with and()

  2. Add the matcher executor to MatcherExecutors.ts:

    export const MatcherExecutors: {
      [T in AnyCaseNodeType]: MatcherExecutor<T>;
    } = {
      // ...etc
      [ARRAY_LENGTH_MATCHER_TYPE]: ArrayLengthExecutor,
    };

Create the matcher DSL

Create a DSL class in packages/case-definition-dsl that creates your matcher type, for example:

/**
 * Everything inside this matcher will be matched exactly, unless overridden
 * with a generic matcher (eg `AnyString` or` ShapedLike`). Use this to switch
 * out of `shapedLike` and back to the default exact matching.
 */
export class ExactlyLike extends CascadingContextMatcher {
  /**
   * @param content - The object, array, primitive or matcher to match exactly
   */
  constructor(content: AnyMatcherOrData) {
    super(content, { matchBy: 'exact' }, {});
  }

  /**
   * For non-TypeScript implementations (see `AnyMatcher.toJSON`)
   *
   * @privateRemarks
   * This comment and the implementation is boilerplate on all matchers to avoid
   * outputting duplicate unimportant documentation on all matcher classes of
   * the docs. Only modify this comment or the implementation via search and replace.
   */
  override toJSON(): unknown {
    return super.toJSON();
  }
}

You can also add a DSL function to the Jest boundary, for example:

/**
 * Everything inside this matcher will be matched exactly, unless overridden with an `any*` matcher
 *
 * Use this to switch out of `shapedLike` and back to the default exact matching.
 *
 * @param content What
 */
export const exactlyLike = (
  content: AnyCaseNodeOrData,
): CoreCascadingMatcher => ({
  'case:matcher:type': CASCADING_CONTEXT_MATCHER_TYPE,
  'case:matcher:child': content,
  'case:context:matchBy': 'exact',
});

If your matcher needs some double checking or additional processing (eg invoking other matchers to make a composite matcher), do it in the DSL layer. The matcher functions in the entities layer are intended to be data only.

Test it

Add or create additional tests at the top level (see index.*.spec.ts for examples)