Skip to content

Commit

Permalink
feat: Load matchers from plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyJones committed Jun 15, 2024
1 parent da3ba50 commit 8bd1bf0
Show file tree
Hide file tree
Showing 12 changed files with 128 additions and 44 deletions.
3 changes: 3 additions & 0 deletions packages/case-core/src/core/BaseCaseContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { CaseConfig } from './config/types';
import { DEFAULT_TEST_ID } from './defaultTestId';
import { traversals } from '../diffmatch';
import { coreShapedLike } from '../entities';
import { loadPlugins } from './plugins';

export class BaseCaseContract {
currentContract: ContractData;
Expand Down Expand Up @@ -97,6 +98,8 @@ export class BaseCaseContract {
// TODO: put in a URL to the documentation here
);
}

loadPlugins(this.initialContext);
}

lookupVariable(
Expand Down
1 change: 1 addition & 0 deletions packages/case-core/src/core/executeExample/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './executeExample';
export * from './setup';
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './setupMock';
export * from '../../../../diffmatch/plugins/loadPlugin';
export * from '../../../plugins/mockExecutors';
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
MockData,
} from '@contract-case/case-plugin-base';

import { loadPlugins } from './loadPlugins';
import { MockSetupFns } from '../../../../../diffmatch/types';

const inferMock = <T extends AnyMockDescriptorType>(
Expand Down Expand Up @@ -52,8 +51,6 @@ const executeMock = <T extends AnyMockDescriptorType>(
);
}

loadPlugins(context);

const executor = MockSetup[mockType];
if (!executor) {
throw new CaseCoreError(`Missing setup for mock type '${mockType}'`);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
CaseConfigurationError,
} from '@contract-case/case-plugin-base';
import { mockExecutor } from './mockExecutor';
import { MockExecutors } from './mockExecutors';
import { MockExecutors } from '../../../plugins/mockExecutors';
import { Assertable } from '../../../../entities/types';

export const setupMock = <T extends AnyMockDescriptorType>(
Expand Down
1 change: 1 addition & 0 deletions packages/case-core/src/core/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './loadPlugins';
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
} from '@contract-case/case-core-plugin-http-dsl';
import { MatchContext } from '@contract-case/case-plugin-base';

import { MockExecutors } from '../mockExecutors';
import { loadPlugin } from '../../../../../diffmatch';
import { loadPlugin } from '../../diffmatch';
import { MockExecutors } from './mockExecutors';

const DEFAULT_PLUGINS = [CoreHttpPlugin] as const;

Expand Down
3 changes: 3 additions & 0 deletions packages/case-core/src/core/plugins/mockExecutors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { MockSetupFns } from '../../diffmatch/plugins/types';

export const MockExecutors: MockSetupFns = {} as MockSetupFns;
28 changes: 6 additions & 22 deletions packages/case-core/src/diffmatch/matching/MatcherExecutors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,7 @@ import {
MatcherExecutor,
LOOKUP_MATCHER_TYPE,
} from '@contract-case/case-plugin-base';
import {
HTTP_STATUS_CODE_MATCHER_TYPE,
HTTP_RESPONSE_MATCHER_TYPE,
HTTP_REQUEST_MATCHER_TYPE,
URL_ENCODED_STRING_TYPE,
HTTP_BASIC_AUTH_TYPE,
} from '@contract-case/case-core-plugin-http-dsl';
import {
HttpStatusCodeMatcher,
HttpResponseMatcher,
HttpRequestMatcher,
UrlEncodedStringMatcher,
HttpBasicAuthMatcher,
} from '@contract-case/case-core-plugin-http';

import {
NumberMatcher,
StringMatcher,
Expand Down Expand Up @@ -70,9 +57,11 @@ import {
StringSuffixMatcher,
} from './strings';

export const MatcherExecutors: {
type AllExecutors = {
[T in AnyCaseNodeType]: MatcherExecutor<T, CaseNodeFor<T>>;
} = {
};

export const MatcherExecutors: AllExecutors = {
[NUMBER_MATCHER_TYPE]: NumberMatcher,
[STRING_MATCHER_TYPE]: StringMatcher,
[STRING_CONTAINS_TYPE]: StringContainsMatcher,
Expand All @@ -83,9 +72,6 @@ export const MatcherExecutors: {
[NULL_MATCHER_TYPE]: NullMatcher,
[SHAPED_ARRAY_MATCHER_TYPE]: ShapedArrayExecutor,
[SHAPED_OBJECT_MATCHER_TYPE]: ShapedObjectExecutor,
[HTTP_STATUS_CODE_MATCHER_TYPE]: HttpStatusCodeMatcher,
[HTTP_RESPONSE_MATCHER_TYPE]: HttpResponseMatcher,
[HTTP_REQUEST_MATCHER_TYPE]: HttpRequestMatcher,
[LOOKUP_MATCHER_TYPE]: LookupMatcher,
[ARRAY_LENGTH_MATCHER_TYPE]: ArrayLengthExecutor,
[COMBINE_MATCHERS_TYPE]: AndCombinationMatcher,
Expand All @@ -95,8 +81,6 @@ export const MatcherExecutors: {
[INTEGER_MATCH_TYPE]: IntegerMatcher,
[OBJECT_KEYS_MATCH_TYPE]: ObjectEachKeyMatches,
[CONTEXT_VARIABLE_TYPE]: ContextVariableMatcher,
[URL_ENCODED_STRING_TYPE]: UrlEncodedStringMatcher,
[HTTP_BASIC_AUTH_TYPE]: HttpBasicAuthMatcher,
[JSON_STRINGIFIED_TYPE]: JsonStringifiedString,
[BASE64_ENCODED_TYPE]: Base64EncodedStringMatcher,
};
} as AllExecutors; // TODO: Remove this assertion when we have everything loaded via plugin
39 changes: 39 additions & 0 deletions packages/case-core/src/diffmatch/plugins/loadPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { CaseConfigurationError } from '@contract-case/case-plugin-base';
import { MatcherExecutors } from '../matching/MatcherExecutors';
import { loadPlugin } from './loadPlugin';
import { EMPTY_MATCH_CONTEXT } from '../../__tests__/testContext';
import { MockSetupFns } from './types';

describe('plugin loader', () => {
beforeEach(() => {
Object.keys(MatcherExecutors).forEach((key) => {
// clear all executors
delete MatcherExecutors[key as keyof typeof MatcherExecutors];
});
});

describe('with LOAD_IN_PROGRESS as the version name', () => {
it('throws a configuration error', () => {
expect(() => {
loadPlugin({} as MockSetupFns, EMPTY_MATCH_CONTEXT, {
name: 'Empty test plugin',
version: 'LOAD_IN_PROGRESS',
matcherExecutors: {},
setupMocks: {},
});
}).toThrow(CaseConfigurationError);
});
});
describe('with a valid version', () => {
it('loads an empty plugin', () => {
expect(() => {
loadPlugin({} as MockSetupFns, EMPTY_MATCH_CONTEXT, {
name: 'Empty test plugin',
version: '0.0.0',
matcherExecutors: {},
setupMocks: {},
});
}).not.toThrow();
});
});
});
83 changes: 71 additions & 12 deletions packages/case-core/src/diffmatch/plugins/loadPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,31 @@ import {
LogContext,
MockExecutorFn,
CORE_PLUGIN_PREFIX,
MatcherExecutor,
} from '@contract-case/case-plugin-base';

import { CoreNumberMatcher } from '@contract-case/case-entities-internal';
import { caseVersion } from '../../entities/caseVersion';
import { MockSetupFns } from './types';
import { MatcherExecutors } from '../matching/MatcherExecutors';

const typeToPluginName: Record<string, string> = {};

const loadedPluginVersions: Record<string, string> = {};

const isCorePlugin = <
MatchT extends string,
MockT extends string,
MatchD extends IsCaseNodeForType<MatchT>,
MockD extends AnyMockDescriptor,
>(
plugin: ContractCasePlugin<MatchT, MockT, MatchD, MockD, unknown>,
): boolean => plugin.name.startsWith(CORE_PLUGIN_PREFIX);

const isCoreType = (type: string): boolean => type.startsWith('_case:');

const IN_PROGRESS = 'LOAD_IN_PROGRESS';

export const loadPlugin = <
MatchT extends string,
MockT extends string,
Expand All @@ -26,16 +42,14 @@ export const loadPlugin = <
context: LogContext,
plugin: ContractCasePlugin<MatchT, MockT, MatchD, MockD, unknown>,
): void => {
if (plugin.version === IN_PROGRESS) {
throw new CaseConfigurationError(
`The plugin '${plugin.name}' reported its version to be LOAD_IN_PROGRESS, which is not valid. Contact the plugin authors to fix this.`,
);
}

if (loadedPluginVersions[plugin.name] != null) {
if (plugin.version !== loadedPluginVersions[plugin.name]) {
if (
plugin.name.startsWith(CORE_PLUGIN_PREFIX) &&
plugin.version !== caseVersion
) {
throw new CaseCoreError(
`Core plugin '${plugin.name}' is at version '${plugin.version}', but this is Core version ${caseVersion}. This isn't supposed to happen.`,
);
}
throw new CaseConfigurationError(
`Trying to load plugin '${plugin.name}' at version '${plugin.version}', but it was previously loaded as version '${loadedPluginVersions[plugin.name]}'.`,
);
Expand All @@ -45,7 +59,16 @@ export const loadPlugin = <
);
return;
}
if (plugin.name.startsWith(CORE_PLUGIN_PREFIX)) {
// We record this at the start, as otherwise failed plugins cause errors every time
loadedPluginVersions[plugin.name] = plugin.version;

if (isCorePlugin(plugin)) {
if (plugin.version !== caseVersion) {
throw new CaseCoreError(
`Core plugin '${plugin.name}' is at version '${plugin.version}', but this is Core version ${caseVersion}. This isn't supposed to happen.`,
);
}

context.logger.deepMaintainerDebug(`Loading core plugin '${plugin.name}'`);
} else {
context.logger.debug(
Expand All @@ -56,14 +79,24 @@ export const loadPlugin = <
Object.entries(plugin.setupMocks).forEach(([mockType, setup]) => {
if (mockType in MockExecutors) {
throw new CaseConfigurationError(
`Plugin '${plugin.name} @ ${plugin.version}' attempted to load a mock setup function for '${mockType}', but one had already been loaded by plugin '${typeToPluginName[mockType]}'.`,
`Plugin '${plugin.name}' @ ${plugin.version} attempted to load a mock setup function for '${mockType}', but one had already been loaded by plugin '${typeToPluginName[mockType]}'.`,
);
}
if (mockType.startsWith(`_case:`)) {
if (isCorePlugin(plugin)) {
if (!isCoreType(mockType)) {
throw new CaseCoreError(
`Core plugin '${plugin.name}' @ ${plugin.version}' tried to load a non-core mock, '${mockType}'`,
);
}
context.logger.deepMaintainerDebug(
`Core plugin '${plugin.name} @ ${plugin.version}' registered a mock setup function with type '${mockType}'`,
`Core plugin '${plugin.name}' @ ${plugin.version}' registered a mock setup function with type '${mockType}'`,
);
} else {
if (isCoreType(mockType)) {
throw new CaseConfigurationError(
`Non-core plugin '${plugin.name} @ ${plugin.version}' tried to load a core mock, '${mockType}'. This is an error in the plugin definition, please contact the plugin's authors`,
);
}
context.logger.debug(
`Plugin '${plugin.name} @ ${plugin.version}' registered a mock setup function with type '${mockType}'`,
);
Expand All @@ -81,5 +114,31 @@ export const loadPlugin = <
typeToPluginName[mockType] = plugin.name;
});

Object.entries(plugin.matcherExecutors).forEach(
([pluginExecutorType, pluginExecutor]) => {
if (pluginExecutorType in MatcherExecutors) {
throw new CaseConfigurationError(
`Plugin '${plugin.name} @ ${plugin.version}' attempted to load a matcher executor for '${pluginExecutorType}', but one had already been loaded by plugin '${typeToPluginName[pluginExecutorType]}'.`,
);
}
if (pluginExecutorType.startsWith(`_case:`)) {
context.logger.deepMaintainerDebug(
`Core plugin '${plugin.name} @ ${plugin.version}' registered a matcher executor with type '${pluginExecutorType}'`,
);
} else {
context.logger.debug(
`Plugin '${plugin.name} @ ${plugin.version}' registered a matcher executor with type '${pluginExecutorType}'`,
);
}

// We cheat the type system here - we can't actually
// do any type checking so we just tell typescript it's any known type.
MatcherExecutors[pluginExecutorType as '_case:MatchNumber'] =
pluginExecutor as MatcherExecutor<
'_case:MatchNumber',
CoreNumberMatcher
>;
},
);
loadedPluginVersions[plugin.name] = plugin.version;
};

0 comments on commit 8bd1bf0

Please sign in to comment.