Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose cache to custom functions #1078

Merged
merged 4 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/guides/custom-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,30 @@ module.exports = (targetVal, _opts, paths) => {
};
```

Furthermore, if your function performs any fs calls to obtain dictionaries and similar, you may want to leverage cache.
P0lip marked this conversation as resolved.
Show resolved Hide resolved
Each custom function is provided with its **own** cache instance that has a function-live lifespan.
What does "function-live lifespan" mean? It means the cache is persisted for the whole life of a particular function.
The cache will be retained between subsequent function calls and is never invalidated unless you compile the function again, i.e. load a ruleset again.
In other words:
- if you Spectral programmatically via JS API and your ruleset remains unchanged, all subsequent `spectral.run` calls will invoke custom functions with the same cache instance.
As soon as you set a ruleset using `setRuleset` or `loadRuleset` method, each custom function will receive a new cache instance.
- if you are a CLI user, the cache will never be invalidated during the timespan of process.

Please bear in mind that while you are welcome to store certain kind of data, using cache for exchanging information between subsequent function calls it strongly discouraged.
Spectral does not guarantee any particular order of execution meaning the functions can be executed in random order, depending on the rules you have, and the document you lint.

```js
module.exports = function () {
if (!this.cache.has('cached-item')) {
this.cache.set('cached-item', anyValue);
}

const cached = this.cache.get('cached-item');

// the rest of function
}
```

## Inheritance

Core functions can be overridden with custom rulesets, so if you'd like to make your own truthy go ahead. Custom functions are only available in the ruleset which defines them, so loading a foo in one ruleset will not clobber a foo in another ruleset.
Expand Down
100 changes: 100 additions & 0 deletions src/__tests__/linter.jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ describe('Linter', () => {
spectral = new Spectral();
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should make use of custom functions', async () => {
await spectral.loadRuleset(customFunctionOASRuleset);
expect(await spectral.run({})).toEqual([
Expand Down Expand Up @@ -66,6 +70,102 @@ describe('Linter', () => {
);
});

it('should expose function-live lifespan cache to custom functions', async () => {
P0lip marked this conversation as resolved.
Show resolved Hide resolved
const logSpy = jest.spyOn(global.console, 'log').mockImplementation(Function);

await spectral.setRuleset({
exceptions: {},
rules: {
foo: {
given: '$',
then: {
function: 'fn',
},
},
bar: {
given: '$',
then: {
function: 'fn',
},
},
},
functions: {
fn: {
source: null,
name: 'fn',
schema: null,
code: `module.exports = function() {
console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
}`,
},
},
});

await spectral.run({});

// verifies whether the 2 subsequent calls passed the same cache instance as the first argument
expect(logSpy.mock.calls[0][0]).toBe(logSpy.mock.calls[1][0]);

await spectral.run({});

expect(logSpy.mock.calls[2][0]).toBe(logSpy.mock.calls[3][0]);
expect(logSpy.mock.calls[0][0]).toBe(logSpy.mock.calls[2][0]);
});

it('should expose cache to custom functions that is not shared among them', async () => {
const logSpy = jest.spyOn(global.console, 'log').mockImplementation(Function);

await spectral.setRuleset({
exceptions: {},
rules: {
foo: {
given: '$',
then: {
function: 'fn',
},
},
bar: {
given: '$',
then: {
function: 'fn-2',
},
},
},
functions: {
fn: {
source: null,
name: 'fn',
schema: null,
code: `module.exports = function() {
console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
}`,
},
'fn-2': {
source: null,
name: 'fn-2',
schema: null,
code: `module.exports = function() {
console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
}`,
},
},
});

await spectral.run({});

// verifies whether the 2 subsequent calls **DID NOT** pass the same cache instance as the first argument
expect(logSpy.mock.calls[0][0]).not.toBe(logSpy.mock.calls[1][0]);

await spectral.run({});

// verifies whether the 2 subsequent calls **DID NOT** pass the same cache instance as the first argument
expect(logSpy.mock.calls[2][0]).not.toBe(logSpy.mock.calls[3][0]);

// verifies whether the 2 subsequent calls to the same function passe the same cache instance as the first argument
expect(logSpy.mock.calls[0][0]).toBe(logSpy.mock.calls[2][0]);
expect(logSpy.mock.calls[1][0]).toBe(logSpy.mock.calls[3][0]);
});

it('should support require calls', async () => {
await spectral.loadRuleset(customFunctionOASRuleset);
expect(
Expand Down
1 change: 1 addition & 0 deletions src/spectral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export class Spectral {

const context: IFunctionContext = {
functions: this.functions,
cache: new Map(),
};

fns[key] = setFunctionContext(context, compileExportedFunction(code, name, source, schema));
Expand Down
1 change: 1 addition & 0 deletions src/types/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CoreFunctions } from '../functions';

export interface IFunctionContext {
functions: CoreFunctions;
cache: Map<unknown, unknown>;
}

export type IFunction<O = any> = (
Expand Down