From 4e77e183ccb22935fbbbe71a277b9fa739e9ff07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 10 Apr 2020 18:23:05 +0200 Subject: [PATCH 1/3] feat: expose cache to custom functions --- docs/guides/custom-functions.md | 12 ++++++++ src/__tests__/linter.jest.test.ts | 46 +++++++++++++++++++++++++++++++ src/spectral.ts | 1 + src/types/function.ts | 1 + 4 files changed, 60 insertions(+) diff --git a/docs/guides/custom-functions.md b/docs/guides/custom-functions.md index a46d1deea..1c1d4d339 100644 --- a/docs/guides/custom-functions.md +++ b/docs/guides/custom-functions.md @@ -233,6 +233,18 @@ 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. + +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. + ## 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. diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts index 232c4effd..89cae9b20 100644 --- a/src/__tests__/linter.jest.test.ts +++ b/src/__tests__/linter.jest.test.ts @@ -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([ @@ -66,6 +70,48 @@ 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 support require calls', async () => { await spectral.loadRuleset(customFunctionOASRuleset); expect( diff --git a/src/spectral.ts b/src/spectral.ts index 68c161c1b..ebcd71b5f 100644 --- a/src/spectral.ts +++ b/src/spectral.ts @@ -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)); diff --git a/src/types/function.ts b/src/types/function.ts index 356167df6..25729b55d 100644 --- a/src/types/function.ts +++ b/src/types/function.ts @@ -4,6 +4,7 @@ import { CoreFunctions } from '../functions'; export interface IFunctionContext { functions: CoreFunctions; + cache: Map; } export type IFunction = ( From 5f1394b598e38707f31e23c81273c6075b6a3623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sat, 11 Apr 2020 23:56:34 +0200 Subject: [PATCH 2/3] docs: tweak --- docs/guides/custom-functions.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/guides/custom-functions.md b/docs/guides/custom-functions.md index 1c1d4d339..e1572bede 100644 --- a/docs/guides/custom-functions.md +++ b/docs/guides/custom-functions.md @@ -240,11 +240,23 @@ The cache will be retained between subsequent function calls and is never invali 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. +- 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. From 4283a8e03e8be43f833822f10139491223d84757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sat, 11 Apr 2020 23:59:34 +0200 Subject: [PATCH 3/3] test: extra test case --- src/__tests__/linter.jest.test.ts | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts index 89cae9b20..9f491fe77 100644 --- a/src/__tests__/linter.jest.test.ts +++ b/src/__tests__/linter.jest.test.ts @@ -112,6 +112,60 @@ console.log(this.cache.get('test') || this.cache.set('test', []).get('test')); 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(