Skip to content

Commit

Permalink
feat: expose cache to custom functions (#1078)
Browse files Browse the repository at this point in the history
* feat: expose cache to custom functions

* docs: tweak

* test: extra test case
  • Loading branch information
P0lip authored Apr 12, 2020
1 parent f9a30ee commit a59aefa
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 0 deletions.
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.
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 () => {
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

0 comments on commit a59aefa

Please sign in to comment.