Plugin Framework
This document is currently inaccurate, as it was written before JSii was found to be unsuitable for writing plugins.
It's still possible to write extensions - if you are planning an extension, please get in touch by opening an issue.
You can also see the core plugin packages here - any package with core-plugin
in the name will provide a good starting point.
There are old instructions for adding matchers in the maintainer documentation.
Caveats for extending ContractCase
Extensions can be written in any of the languages where ContractCase is available. However, if you wish to distribute your extension for use by others who don't use the same language, it must be written in TypeScript and transpiled with JSii.
Additionally, if your matcher is likely to be of general use, consider making a pull request to add it to the core implementation.
Anatomy of a ContractCase Example
TODO: Describe case example file structure
ContractCase Context
TODO: Describe the context object, which is passed to all matchers and mock executors.
Extending with a new matcher type
To extend case with a new Matcher type:
- Implement the description object. This is the object that will be produced by the DSL, and the content that will be written to the contract file.
- Implement the corresponding
MatcherExecutor
Designing the description object
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 YOUR_CUSTOM_MATCHER_TYPE = 'yourName:yourMatcher' as const;
Note that all matchers that Case provides are prefixed with case:
. To avoid
clashing with official matcher names, this prefix is not allowed in extensions.
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 CORE_ARRAY_LENGTH_MATCHER;
'case:matcher:minLength': number;
'case:matcher:maxLength': number;
}
If your matcher modifies the context object, add fields prefixed with
case:context:
- these are automatically picked up by ContractCase and rolled
into the context before this matcher is invoked. Because case matchers are
recursive, this context is passed down to any child matchers.
Implementing the description object
Create a DSL function that creates your matcher type, 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',
});
Implementing the MatcherExecutor
Next, we will add the behaviour of the matcher, both for matching, and for stripping the matchers.
Implement a type that satisfies MatcherExecutor<typeof YOUR_NEW_TYPE_STRING>
. For example:
const strip: StripMatcherFn<typeof YOUR_CUSTOM_MATCHER_TYPE> = (
matcher: YourCustomMatcherInterface,
matchContext: MatchContext
): AnyData => // implement the strip matcher function here
const check: CheckMatchFn<typeof YOUR_CUSTOM_MATCHER_TYPE> = (
matcher: YourCustomMatcherInterface,
matchContext: MatchContext,
actual: unknown
): Promise<MatchResult> | MatchResult => // Implement your check here
export const ArrayLengthExecutor: MatcherExecutor<
typeof YOUR_CUSTOM_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 MatcherExecutor implementations for examples.
If your matcher doesn't have enough context to strip matchers (eg, for
auxiliary 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(...)
Extending with a new mock type
To extend case with a new Mock type:
- Implement a function that matches the
MockSetupFn
interface to give ContractCase the behaviour - Implement a function that returns a json-serialisable object that the function you created in the previous step would expect.
ContractCase Contract Format
Most users do not need to know the format - you can treat the contract file as opaque. TODO: Describe the format