Skip to content

Commit

Permalink
feat(ruleset-migrator): inline external rulesets & support exceptions (
Browse files Browse the repository at this point in the history
…#1711)

* fix(ruleset-migrator): more proper npmCustomRegistry support & drop functions prop in output

* feat(ruleset-migrator): inline external rulesets

* feat(ruleset-migrator): cover exceptions
  • Loading branch information
P0lip committed Jul 6, 2021
1 parent 45b15a2 commit 2a1d2d3
Show file tree
Hide file tree
Showing 28 changed files with 491 additions and 129 deletions.
1 change: 0 additions & 1 deletion packages/cli/src/services/linter/utils/getRuleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export async function getRuleset(rulesetFile: Optional<string>): Promise<Ruleset
await AsyncFunction(
'module, require',
await migrateRuleset(rulesetFile, {
cwd: process.cwd(),
format: 'commonjs',
fs,
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/ruleset-migrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"astring": "^1.7.5"
},
"devDependencies": {
"@stoplight/spectral-core": "^0.0.0",
"@stoplight/spectral-rulesets": "^0.0.0",
"json-schema-to-typescript": "^10.1.4",
"memfs": "^3.2.2",
"prettier": "^2.3.2"
Expand Down
16 changes: 14 additions & 2 deletions packages/ruleset-migrator/scripts/generate-test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,27 @@ fs.promises.readdir(cwd).then(async ls => {
fs.promises.readFile(path.join(dirpath, 'output.cjs'), 'utf8').then(assign(bundle, 'output.cjs')),
fs.promises.readFile(path.join(dirpath, 'output.mjs'), 'utf8').then(assign(bundle, 'output.mjs')),
fs.promises.readFile(path.join(dirpath, 'ruleset.yaml'), 'utf8').then(assign(bundle, 'ruleset')),
fs.promises
.readdir(path.join(dirpath, 'assets'))
.then(list =>
Promise.all(
list.map(item =>
fs.promises.readFile(path.join(dirpath, 'assets', item), 'utf8').then(assign(bundle, `assets/${item}`)),
),
),
)
.catch(() => {
// it may not exist
}),
);
}

await Promise.all(promises);
await fs.promises.writeFile(path.join(cwd, '.cache/index.json'), JSON.stringify(bundled, null, 2));
});

function assign(bundled: Record<string, string>, kind: string) {
function assign(bundled: Record<string, string>, name: string) {
return async (input: string): Promise<void> => {
bundled[kind] = kind === 'ruleset' ? (input as string) : prettier.format(input as string, { parser: 'babel' });
bundled[name] = /\.[mc]js$/.test(name) ? prettier.format(input as string, { parser: 'babel' }) : (input as string);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { oas: oas } = require("@stoplight/spectral-rulesets");
module.exports = {
extends: oas,
overrides: [
{
files: ["subfolder/one.yaml#"],
rules: {
"no-$ref-siblings": "off",
},
},
{
files: ["/tmp/docs/one.yaml#/info"],
rules: {
"info-contact": "off",
"info-description": "off",
},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { oas } from "@stoplight/spectral-rulesets";
export default {
extends: oas,
overrides: [
{
files: ["subfolder/one.yaml#"],
rules: {
"no-$ref-siblings": "off",
},
},
{
files: ["/tmp/docs/one.yaml#/info"],
rules: {
"info-contact": "off",
"info-description": "off",
},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends: spectral:oas

except:
"subfolder/one.yaml#":
- no-$ref-siblings
"/tmp/docs/one.yaml#/info":
- info-contact
- info-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
rules:
my-rule:
given: $
then:
function: truthy
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
const { migrateRuleset: migrateRuleset } = require('@stoplight/spectral-ruleset-migrator');
const { truthy: truthy } = require('@stoplight/spectral-functions');
module.exports = {
extends: await migrateRuleset('https://stoplight.io/ruleset.json'),
extends: [
{
rules: {
'my-rule': {
given: '$',
then: {
function: truthy,
},
},
},
},
],
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { migrateRuleset } from '@stoplight/spectral-ruleset-migrator';
import { truthy } from '@stoplight/spectral-functions';
export default {
extends: await migrateRuleset('https://stoplight.io/ruleset.json'),
extends: [
{
rules: {
'my-rule': {
given: '$',
then: {
function: truthy,
},
},
},
},
],
};
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
{ 'extends': 'https://stoplight.io/ruleset.json' }
extends:
- ./assets/ruleset.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
const { oas2: oas2, oas3: oas3 } = require('@stoplight/spectral-formats');
const oasDocumentSchema = _interopDefault(require('./functions/oasDocumentSchema.js'));
const oasExample = _interopDefault(require('./functions/oasExample.js'));
const oasOp2xxResponse = _interopDefault(require('./functions/oasOp2xxResponse.js'));
const oasOpFormDataConsumeCheck = _interopDefault(require('./functions/oasOpFormDataConsumeCheck.js'));
const typedEnum = _interopDefault(require('./functions/typedEnum.js'));
const refSiblings = _interopDefault(require('./functions/refSiblings.js'));
const oasDocumentSchema = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/oasDocumentSchema.js'));
const oasExample = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/oasExample.js'));
const oasOp2xxResponse = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/oasOp2xxResponse.js'));
const oasOpFormDataConsumeCheck = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/oasOpFormDataConsumeCheck.js'));
const typedEnum = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/typedEnum.js'));
const refSiblings = _interopDefault(require('/.tmp/spectral/functions-variant-1/functions/refSiblings.js'));
module.exports = {
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/openapi-rules.md',
formats: [oas2, oas3],
functions: [oasDocumentSchema, oasExample, oasOp2xxResponse, oasOpFormDataConsumeCheck, typedEnum, refSiblings],
rules: {
'operation-2xx-response': {
description: 'Operation must have at least one `2xx` response.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { oas2, oas3 } from '@stoplight/spectral-formats';
import oasDocumentSchema from './functions/oasDocumentSchema.js';
import oasExample from './functions/oasExample.js';
import oasOp2xxResponse from './functions/oasOp2xxResponse.js';
import oasOpFormDataConsumeCheck from './functions/oasOpFormDataConsumeCheck.js';
import typedEnum from './functions/typedEnum.js';
import refSiblings from './functions/refSiblings.js';
import oasDocumentSchema from '/.tmp/spectral/functions-variant-1/functions/oasDocumentSchema.js';
import oasExample from '/.tmp/spectral/functions-variant-1/functions/oasExample.js';
import oasOp2xxResponse from '/.tmp/spectral/functions-variant-1/functions/oasOp2xxResponse.js';
import oasOpFormDataConsumeCheck from '/.tmp/spectral/functions-variant-1/functions/oasOpFormDataConsumeCheck.js';
import typedEnum from '/.tmp/spectral/functions-variant-1/functions/typedEnum.js';
import refSiblings from '/.tmp/spectral/functions-variant-1/functions/refSiblings.js';
export default {
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/openapi-rules.md',
formats: [oas2, oas3],
functions: [oasDocumentSchema, oasExample, oasOp2xxResponse, oasOpFormDataConsumeCheck, typedEnum, refSiblings],
rules: {
'operation-2xx-response': {
description: 'Operation must have at least one `2xx` response.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const oasDocumentSchema = _interopDefault(require('./custom-functions/oasDocumentSchema.js'));
const oasExample = _interopDefault(require('./custom-functions/oasExample.js'));
const oasOp2xxResponse = _interopDefault(require('./custom-functions/oasOp2xxResponse.js'));
const oasDocumentSchema = _interopDefault(require('/.tmp/spectral/functions-variant-2/custom-functions/oasDocumentSchema.js'));
const oasExample = _interopDefault(require('/.tmp/spectral/functions-variant-2/custom-functions/oasExample.js'));
const oasOp2xxResponse = _interopDefault(require('/.tmp/spectral/functions-variant-2/custom-functions/oasOp2xxResponse.js'));
module.exports = {
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/openapi-rules.md',
functions: [oasDocumentSchema, oasExample, oasOp2xxResponse],
rules: {
'operation-2xx-response': {
description: 'Operation must have at least one `2xx` response.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import oasDocumentSchema from './custom-functions/oasDocumentSchema.js';
import oasExample from './custom-functions/oasExample.js';
import oasOp2xxResponse from './custom-functions/oasOp2xxResponse.js';
import oasDocumentSchema from '/.tmp/spectral/functions-variant-2/custom-functions/oasDocumentSchema.js';
import oasExample from '/.tmp/spectral/functions-variant-2/custom-functions/oasExample.js';
import oasOp2xxResponse from '/.tmp/spectral/functions-variant-2/custom-functions/oasOp2xxResponse.js';
export default {
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/openapi-rules.md',
functions: [oasDocumentSchema, oasExample, oasOp2xxResponse],
rules: {
'operation-2xx-response': {
description: 'Operation must have at least one `2xx` response.',
Expand Down
142 changes: 138 additions & 4 deletions packages/ruleset-migrator/src/__tests__/ruleset.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { fs } from 'memfs';
import * as path from 'path';
import * as prettier from 'prettier';
import * as prettier from 'prettier/standalone';
import * as parserBabel from 'prettier/parser-babel';
import { Ruleset } from '@stoplight/spectral-core';
import { DiagnosticSeverity } from '@stoplight/types';

import { migrateRuleset } from '..';
import * as fixtures from './__fixtures__/.cache/index.json';
Expand All @@ -23,6 +26,7 @@ describe('migrator', () => {
beforeAll(async () => {
await fs.promises.mkdir(dir, { recursive: true });
for (const [name, content] of Object.entries(entries)) {
await fs.promises.mkdir(path.join(dir, path.dirname(name)), { recursive: true });
await fs.promises.writeFile(path.join(dir, name), content);
}
});
Expand All @@ -38,26 +42,156 @@ describe('migrator', () => {
expect(
prettier.format(
await migrateRuleset(ruleset, {
cwd,
format,
fs: fs as any,
}),
{ parser: 'babel' },
{ parser: 'babel', plugins: [parserBabel] },
),
).toEqual(await fs.promises.readFile(path.join(dir, `output${ext}`), 'utf8'));
});
});

it('should support subsequent migrations', async () => {
await fs.promises.writeFile(
path.join(cwd, 'ruleset-migration-1.json'),
JSON.stringify({
extends: ['./ruleset-migration-2.json'],
rules: {
'valid-type': 'error',
},
}),
);

await fs.promises.writeFile(
path.join(cwd, 'ruleset-migration-2.json'),
JSON.stringify({
extends: 'spectral:oas',
rules: {
'valid-type': {
given: '$',
then: {
function: 'truthy',
},
},
},
}),
);

const _module: { exports?: any } = {};

await (async () => void 0).constructor(
'module, require',
await migrateRuleset(path.join(cwd, 'ruleset-migration-1.json'), {
format: 'commonjs',
fs: fs as any,
}),
)(_module, (id: string): unknown => {
switch (id) {
case '@stoplight/spectral-functions':
return require('@stoplight/spectral-functions') as unknown;
case '@stoplight/spectral-rulesets':
return require('@stoplight/spectral-rulesets') as unknown;
default:
throw new ReferenceError(`${id} not found`);
}
});

const ruleset = new Ruleset(_module.exports);

expect(Object.keys(ruleset.rules)).toEqual([
...Object.keys(require('@stoplight/spectral-rulesets').oas.rules),
'valid-type',
]);

expect(ruleset.rules['valid-type'].severity).toEqual(DiagnosticSeverity.Error);
});

describe('error handling', () => {
it('given unknown format, should throw', async () => {
await fs.promises.writeFile(path.join(cwd, 'unknown-format.json'), `{ "formats": ["json-schema-draft-2"] }`);
await expect(
migrateRuleset(path.join(cwd, 'unknown-format.json'), {
cwd,
format: 'esm',
fs: fs as any,
}),
).rejects.toThrow('Invalid ruleset provided');
});
});

describe('custom npm registry', () => {
it('should be supported', async () => {
await fs.promises.writeFile(
path.join(cwd, 'custom-npm-provider.json'),
JSON.stringify({
extends: 'spectral:asyncapi',
formats: ['oas2'],
rules: {
rule: {
then: {
given: '$',
function: 'truthy',
},
},
},
}),
);
expect(
await migrateRuleset(path.join(cwd, 'custom-npm-provider.json'), {
format: 'esm',
fs: fs as any,
npmRegistry: 'https://unpkg.com/',
}),
).toEqual(`import {asyncapi} from "https://unpkg.com/@stoplight/spectral-rulesets";
import {oas2} from "https://unpkg.com/@stoplight/spectral-formats";
import {truthy} from "https://unpkg.com/@stoplight/spectral-functions";
export default {
"extends": asyncapi,
"formats": [oas2],
"rules": {
"rule": {
"then": {
"given": "$",
"function": truthy
}
}
}
};
`);
});

it('should not apply to custom functions', async () => {
await fs.promises.writeFile(
path.join(cwd, 'custom-npm-provider-custom-functions.json'),
JSON.stringify({
functions: ['customFunction'],
rules: {
rule: {
then: {
given: '$',
function: 'customFunction',
},
},
},
}),
);
expect(
await migrateRuleset(path.join(cwd, 'custom-npm-provider-custom-functions.json'), {
format: 'esm',
fs: fs as any,
npmRegistry: 'https://unpkg.com/',
}),
).toEqual(`import customFunction from "/.tmp/spectral/functions/customFunction.js";
export default {
"rules": {
"rule": {
"then": {
"given": "$",
"function": customFunction
}
}
}
};
`);
});
});
});

0 comments on commit 2a1d2d3

Please sign in to comment.