Skip to content

Commit

Permalink
feat(core): serializable rulesets (#1747)
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Jul 14, 2021
1 parent c0880db commit 3f0e842
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 15 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@stoplight/path": "1.3.2",
"@stoplight/spectral-parsers": "^1.0.0",
"@stoplight/spectral-ref-resolver": "*",
"@stoplight/spectral-runtime": "*",
"@stoplight/spectral-runtime": "^1.0.0",
"@stoplight/types": "12.3.0",
"ajv": "~8.6.0",
"ajv-errors": "~3.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import _base from './_base';
import { RulesetDefinition } from '../../../types';

export { ruleset as default };

const ruleset: RulesetDefinition = {
rules: {
..._base.rules,
},
overrides: [
{
files: ['legacy/**/*.json'],
rules: {
'description-matches-stoplight': 'off',
'title-matches-stoplight': 'warn',
'contact-name-matches-stoplight': true,
},
},
{
files: ['v2/**/*.json'],
rules: {
'description-matches-stoplight': 'error',
'title-matches-stoplight': 'hint',
},
},
{
files: ['**/*.json'],
rules: {
'description-matches-stoplight': 'info',
'title-matches-stoplight': 'off',
},
},
],
};
256 changes: 252 additions & 4 deletions packages/core/src/ruleset/__tests__/ruleset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ describe('Ruleset', () => {
}

it('given ruleset with extends set to recommended, should enable recommended rules', async () => {
const { rules } = await loadRuleset(import('./__fixtures__/severity/recommended'));
expect(Object.keys(rules)).toEqual([
const ruleset = await loadRuleset(import('./__fixtures__/severity/recommended'));
expect(Object.keys(ruleset.rules)).toEqual([
'description-matches-stoplight',
'title-matches-stoplight',
'contact-name-matches-stoplight',
'overridable-rule',
]);

expect(getEnabledRules(rules)).toEqual([
expect(getEnabledRules(ruleset.rules)).toEqual([
'description-matches-stoplight',
'title-matches-stoplight',
'overridable-rule',
]);

expect(rules).toStrictEqual((await loadRuleset(import('./__fixtures__/severity/implicit'))).rules);
expect(print(ruleset)).toEqual(print(await loadRuleset(import('./__fixtures__/severity/implicit'))));
});

it('given ruleset with extends set to all, should enable all rules but explicitly disabled', async () => {
Expand Down Expand Up @@ -203,6 +203,41 @@ describe('Ruleset', () => {
└─ severity: 1
`);
});

it('should be serializable', async () => {
expect(JSON.parse(JSON.stringify(await loadRuleset(import('./__fixtures__/circularity/direct'))))).toEqual({
id: expect.any(Number),
source: null,
aliases: null,
extends: [],
formats: null,
overrides: null,
parserOptions: {
duplicateKeys: DiagnosticSeverity.Error,
incompatibleValues: DiagnosticSeverity.Error,
},
rules: {
'foo-rule': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['$'],
message: null,
name: 'foo-rule',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: 1,
then: [
{
function: 'falsy',
},
],
},
},
});
});
});

describe('error handling', () => {
Expand Down Expand Up @@ -474,6 +509,105 @@ describe('Ruleset', () => {
`);
});

it('should be serializable', async () => {
const ruleset = await loadRuleset(import('./__fixtures__/overrides/serializing'), path.join(cwd, 'serializing'));

expect(JSON.parse(JSON.stringify(ruleset))).toEqual({
id: expect.any(Number),
source: path.join(cwd, 'serializing'),
aliases: null,
extends: null,
formats: null,
overrides: [
{
files: ['legacy/**/*.json'],
rules: {
'contact-name-matches-stoplight': true,
'description-matches-stoplight': 'off',
'title-matches-stoplight': 'warn',
},
},
{
files: ['v2/**/*.json'],
rules: {
'description-matches-stoplight': 'error',
'title-matches-stoplight': 'hint',
},
},
{
files: ['**/*.json'],
rules: {
'description-matches-stoplight': 'info',
'title-matches-stoplight': 'off',
},
},
],
parserOptions: {
duplicateKeys: DiagnosticSeverity.Error,
incompatibleValues: DiagnosticSeverity.Error,
},
rules: {
'contact-name-matches-stoplight': {
description: null,
documentationUrl: null,
enabled: false,
formats: null,
given: ['$.info.contact'],
message: 'Contact name must contain Stoplight',
name: 'contact-name-matches-stoplight',
owner: expect.any(Number),
recommended: false,
resolved: true,
severity: DiagnosticSeverity.Warning,
then: [
{
function: 'pattern',
functionOptions: 'Object{}',
},
],
},
'description-matches-stoplight': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['$.info'],
message: 'Description must contain Stoplight',
name: 'description-matches-stoplight',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: DiagnosticSeverity.Error,
then: [
{
function: 'pattern',
functionOptions: 'Object{}',
},
],
},
'title-matches-stoplight': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['$.info'],
message: 'Title must contain Stoplight',
name: 'title-matches-stoplight',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: DiagnosticSeverity.Warning,
then: [
{
function: 'pattern',
functionOptions: 'Object{}',
},
],
},
},
});
});

it('should support new rule definitions', async () => {
const ruleset = await loadRuleset(
import('./__fixtures__/overrides/new-definitions'),
Expand Down Expand Up @@ -922,6 +1056,120 @@ describe('Ruleset', () => {
`);
});

it('should be serializable', () => {
expect(
JSON.parse(
JSON.stringify(
new Ruleset({
aliases: {
Info: '$.info',
PathItem: '$.paths[*][*]',
Description: '$..description',
Name: '$..name',
},

rules: {
'valid-path': {
given: '#PathItem',
then: {
function: truthy,
},
},

'valid-name-and-description': {
given: ['#Name', '#Description'],
then: {
function: truthy,
},
},

'valid-contact': {
given: '#Info.contact',
then: {
function: truthy,
},
},
},
}),
),
),
).toEqual({
extends: null,
source: null,
id: expect.any(Number),
formats: null,
overrides: null,
parserOptions: {
duplicateKeys: DiagnosticSeverity.Error,
incompatibleValues: DiagnosticSeverity.Error,
},
aliases: {
Info: '$.info',
PathItem: '$.paths[*][*]',
Description: '$..description',
Name: '$..name',
},
rules: {
'valid-path': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['#PathItem'],
message: null,
name: 'valid-path',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: DiagnosticSeverity.Warning,
then: [
{
function: 'truthy',
},
],
},

'valid-name-and-description': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['#Name', '#Description'],
message: null,
name: 'valid-name-and-description',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: DiagnosticSeverity.Warning,
then: [
{
function: 'truthy',
},
],
},

'valid-contact': {
description: null,
documentationUrl: null,
enabled: true,
formats: null,
given: ['#Info.contact'],
message: null,
name: 'valid-contact',
owner: expect.any(Number),
recommended: true,
resolved: true,
severity: DiagnosticSeverity.Warning,
then: [
{
function: 'truthy',
},
],
},
},
});
});

it('should resolve nested aliases', () => {
expect(
print(
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/ruleset/rule/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Ruleset } from '../ruleset';
import { Format } from '../format';
import { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition } from '../types';
import { minimatch } from '../utils/minimatch';
import { printValue } from '@stoplight/spectral-runtime';

const ALIAS = /^#([A-Za-z0-9_-]+)/;

Expand All @@ -26,6 +27,13 @@ export interface IRule {
given: string[];
}

export type StringifiedRule = Omit<IRule, 'formats' | 'then'> & {
name: string;
formats: string[] | null;
then: (Pick<IRuleThen, 'field'> & { function: string; functionOptions?: string })[];
owner: number;
};

export class Rule implements IRule {
public description: string | null;
public message: string | null;
Expand Down Expand Up @@ -218,6 +226,27 @@ export class Rule implements IRule {
};
}
}

public toJSON(): StringifiedRule {
return {
name: this.name,
recommended: this.recommended,
enabled: this.enabled,
description: this.description,
message: this.message,
documentationUrl: this.documentationUrl,
severity: this.severity,
resolved: this.resolved,
formats: this.formats === null ? null : Array.from(this.formats).map(fn => fn.displayName ?? fn.name),
then: this.then.map(then => ({
...then.function,
function: then.function.name,
...('functionOptions' in then ? { functionOptions: printValue(then.functionOptions) } : null),
})),
given: this.#given,
owner: this.owner.id,
};
}
}

function stub(): void {
Expand Down

0 comments on commit 3f0e842

Please sign in to comment.