Skip to content

Commit

Permalink
refactor: introduce Rule
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed May 1, 2020
1 parent e798cab commit c40094b
Show file tree
Hide file tree
Showing 66 changed files with 599 additions and 694 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"lodash": "4.17.15",
"nanoid": "2.1.11",
"node-fetch": "2.6",
"promise.allsettled": "1.0.2",
"proxy-agent": "3.1.1",
"strip-ansi": "6.0",
"text-table": "0.2",
Expand Down
13 changes: 6 additions & 7 deletions setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { RulesetExceptionCollection } from './src/types/ruleset';

import { Dictionary } from '@stoplight/types';
import { IRule, IRunRule, isAsyncApiv2, Spectral } from './src';
import { IRule, isAsyncApiv2, Rule, RuleCollection, Spectral } from './src';
import { rules as asyncApiRules } from './src/rulesets/asyncapi/index.json';

export const buildRulesetExceptionCollectionFrom = (
Expand All @@ -20,17 +19,17 @@ const removeAllRulesBut = (spectral: Spectral, ruleName: string) => {

const rawRule = asyncApiRules[ruleName];

const patchedRule: IRule = Object.assign(rule1, {
const patchedRule = Object.assign(rule1, {
recommended: true,
severity: rawRule.severity,
});
}) as IRule;

const rules: Dictionary<IRule, string> = {};
const rules: RuleCollection = {};
rules[ruleName] = patchedRule;
spectral.setRules(rules);
};

export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promise<[Spectral, IRunRule]> => {
export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promise<[Spectral, Rule]> => {
const s = new Spectral();
s.registerFormat('asyncapi2', isAsyncApiv2);
await s.loadRuleset('spectral:asyncapi');
Expand All @@ -39,7 +38,7 @@ export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promi
expect(Object.keys(s.rules)).toEqual([ruleName]);

const rule = s.rules[ruleName];
expect(rule.recommended).not.toBe(false);
expect(rule.enabled).not.toBe(false);
expect(rule.severity).not.toBeUndefined();
expect(rule.severity).not.toEqual(-1);
expect(rule.formats).not.toBeUndefined();
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/__fixtures__/bare-oas-ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@
}
}
}
},
"except": {
"/test/file.json#/info": ["info-contact", "info-description"],
"/test/file.json#": [ "oas3-api-servers"],
"/test/file.json#/paths/~1a.two/get": ["operation-2xx-response"],
"/test/file.json#/paths/~1b.three/get": ["operation-2xx-response"],
"another.yaml#": ["dummy-rule", "info-contact"]
}
}
3 changes: 2 additions & 1 deletion src/__tests__/functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IRule, Spectral } from '../spectral';
import { IRule } from '..';
import { Spectral } from '../spectral';
import { IRuleResult } from '../types';

const applyRuleToObject = async (r: IRule, o: object): Promise<IRuleResult[]> => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/linter.jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));

expect(Object.keys(spectral.rules)).toHaveLength(3);

expect(Object.entries(spectral.rules).map(([name, rule]) => [name, rule.recommended])).toEqual([
expect(Object.entries(spectral.rules).map(([name, rule]) => [name, rule.enabled])).toEqual([
['explicitly-recommended', true],
['implicitly-recommended', true],
['explicitly-not-recommended', false],
Expand Down
60 changes: 31 additions & 29 deletions src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Resolver } from '@stoplight/json-ref-resolver';
import { DiagnosticSeverity } from '@stoplight/types';
import { parse } from '@stoplight/yaml';
import { omit } from 'lodash';
import { IParsedResult } from '../document';
import { isOpenApiv2, isOpenApiv3 } from '../formats';
import { mergeRules, readRuleset } from '../rulesets';
Expand Down Expand Up @@ -150,16 +151,20 @@ describe('linter', () => {
test('should not report anything for disabled rules', async () => {
await spectral.loadRuleset('spectral:oas');
const { rules: oasRules } = await readRuleset('spectral:oas');
spectral.setRules(
mergeRules(oasRules, {
spectral.setRules({
...mergeRules(oasRules, {
'oas3-valid-schema-example': 'off',
'operation-2xx-response': -1,
'openapi-tags': 'off',
}) as RuleCollection,
);

// @ts-ignore
spectral.rules['oas3-schema'].then.function = 'oasDocumentSchema';
'operation-tag-defined': 'off',
}),
...omit(spectral.rules, [
'oas3-valid-schema-example',
'operation-2xx-response',
'openapi-tags',
'operation-tag-defined',
]),
} as RuleCollection);

const result = await spectral.run(invalidSchema);

Expand Down Expand Up @@ -530,9 +535,7 @@ describe('linter', () => {
});

test('should include parser diagnostics', async () => {
await spectral.loadRuleset('spectral:oas');

const responses = `openapi: 2.0.0
const responses = `
responses:: !!foo
400:
description: a
Expand All @@ -542,7 +545,7 @@ responses:: !!foo
description: c
`;

const result = await spectral.run(responses);
const result = await spectral.run(responses, { ignoreUnknownFormat: true });

expect(result).toEqual(
expect.arrayContaining([
Expand Down Expand Up @@ -671,33 +674,31 @@ responses:: !!foo
});

test('should report invalid $refs', async () => {
await spectral.loadRuleset('spectral:oas');

const result = await spectral.run(invalidSchema);

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'invalid-ref',
message: "No resolver defined for scheme 'file' in ref ./models/pet.yaml",
path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
expect.objectContaining({
code: 'invalid-ref',
message: "No resolver defined for scheme 'file' in ref ../common/models/error.yaml",
path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
]),
);
expect(result).toEqual([
expect.objectContaining({
code: 'invalid-ref',
message: "No resolver defined for scheme 'file' in ref ./models/pet.yaml",
path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
expect.objectContaining({
code: 'invalid-ref',
message: "No resolver defined for scheme 'file' in ref ../common/models/error.yaml",
path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
]);
});

test('should support YAML merge keys', async () => {
await spectral.loadRuleset('spectral:oas');
spectral.setRules({
'operation-tag-defined': {
...spectral.rules['operation-tag-defined'],
message: spectral.rules['operation-tag-defined'].message ?? '',
description: spectral.rules['operation-tag-defined'].description ?? '',
severity: 'off',
},
});
Expand Down Expand Up @@ -1024,6 +1025,7 @@ responses:: !!foo
spectral.setRules({
'oas3-schema': {
...spectral.rules['oas3-schema'],
description: spectral.rules['oas3-schema'].description ?? '',
message: 'Schema error at {{path}}',
},
});
Expand Down
45 changes: 15 additions & 30 deletions src/__tests__/spectral.jest.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalize } from '@stoplight/path';
import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
import { DiagnosticSeverity } from '@stoplight/types';
import * as fs from 'fs';
import * as nock from 'nock';
import * as path from 'path';
Expand All @@ -9,11 +9,9 @@ import { isOpenApiv2 } from '../formats';
import { pattern } from '../functions/pattern';
import * as Parsers from '../parsers';
import { httpAndFileResolver } from '../resolvers/http-and-file';
import { IRunRule, Spectral } from '../spectral';
import { Spectral } from '../spectral';

const oasRuleset = require('../rulesets/oas/index.json');
const oasRulesetRules: Dictionary<IRunRule, string> = oasRuleset.rules;
const customOASRuleset = require('./__fixtures__/custom-oas-ruleset.json');
const bareRuleset = require('./__fixtures__/bare-oas-ruleset.json');

describe('Spectral', () => {
afterEach(() => {
Expand All @@ -23,29 +21,16 @@ describe('Spectral', () => {
describe('loadRuleset', () => {
test('should support loading rulesets from filesystem', async () => {
const s = new Spectral();
await s.loadRuleset(path.join(__dirname, '__fixtures__/custom-oas-ruleset.json'));
await s.loadRuleset(path.join(__dirname, '__fixtures__/bare-oas-ruleset.json'));

expect(s.rules).toEqual(
expect.objectContaining({
...[...Object.entries(oasRulesetRules)].reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
oasRules[name] = {
name,
...rule,
formats: expect.arrayContaining([expect.any(String)]),
recommended: expect.any(Boolean),
severity: expect.any(Number),
then: expect.any(Object),
};

return oasRules;
}, {}),
'info-matches-stoplight': {
...customOASRuleset.rules['info-matches-stoplight'],
name: 'info-matches-stoplight',
severity: DiagnosticSeverity.Warning,
},
expect(s.rules).toEqual({
'info-matches-stoplight': expect.objectContaining({
message: bareRuleset.rules['info-matches-stoplight'].message,
name: 'info-matches-stoplight',
given: [bareRuleset.rules['info-matches-stoplight'].given],
severity: DiagnosticSeverity.Warning,
}),
);
});

Object.keys(s.exceptions).forEach(p => expect(path.isAbsolute(p)).toEqual(true));

Expand Down Expand Up @@ -84,12 +69,12 @@ describe('Spectral', () => {
await s.loadRuleset('https://localhost:4000/custom-ruleset');

expect(s.rules).toEqual({
'info-matches-stoplight': {
...ruleset.rules['info-matches-stoplight'],
'info-matches-stoplight': expect.objectContaining({
message: bareRuleset.rules['info-matches-stoplight'].message,
name: 'info-matches-stoplight',
recommended: true,
given: [bareRuleset.rules['info-matches-stoplight'].given],
severity: DiagnosticSeverity.Warning,
},
}),
});
});
});
Expand Down
9 changes: 5 additions & 4 deletions src/__tests__/spectral.karma.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ describe('Spectral', () => {
await s.loadRuleset('https://localhost:4000/custom-ruleset');

expect(s.rules).toEqual({
'info-matches-stoplight': {
...ruleset.rules['info-matches-stoplight'],
'info-matches-stoplight': expect.objectContaining({
given: ['$.info'],
name: 'info-matches-stoplight',
recommended: true,
message: 'Info must contain Stoplight',
enabled: true,
severity: DiagnosticSeverity.Warning,
},
}),
});
});
});
Expand Down
41 changes: 22 additions & 19 deletions src/__tests__/spectral.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { IGraphNodeData } from '@stoplight/json-ref-resolver/types';
import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
import { DiagnosticSeverity } from '@stoplight/types';
import { DepGraph } from 'dependency-graph';
import { escapeRegExp, merge } from 'lodash';

import { buildRulesetExceptionCollectionFrom } from '../../setupTests';
import { Document } from '../document';
import * as Parsers from '../parsers';
import { Spectral } from '../spectral';
import { IResolver, IRunRule } from '../types';
import { RunRuleCollection, Spectral } from '../spectral';
import { IResolver, RuleCollection } from '../types';
import { RulesetExceptionCollection } from '../types/ruleset';

import { buildRulesetExceptionCollectionFrom } from '../../setupTests';

const oasRuleset = JSON.parse(JSON.stringify(require('../rulesets/oas/index.json')));
const asyncApiRuleset = JSON.parse(JSON.stringify(require('../rulesets/asyncapi/index.json')));
const oasRulesetRules: Dictionary<IRunRule, string> = oasRuleset.rules;
const asyncApiRulesetRules: Dictionary<IRunRule, string> = asyncApiRuleset.rules;
const oasRulesetRules: RuleCollection = oasRuleset.rules;
const asyncApiRulesetRules: RuleCollection = asyncApiRuleset.rules;

describe('spectral', () => {
describe('loadRuleset', () => {
Expand All @@ -27,15 +26,17 @@ describe('spectral', () => {

expect(s.rules).toEqual(
expect.objectContaining(
Object.entries(rules).reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
oasRules[name] = {
Object.entries(rules).reduce<RunRuleCollection>((oasRules, [name, rule]) => {
oasRules[name] = expect.objectContaining({
name,
...rule,
given: expect.anything(),
formats: expect.arrayContaining([expect.any(String)]),
recommended: expect.any(Boolean),
enabled: expect.any(Boolean),
severity: expect.any(Number),
then: expect.any(Object),
};
then: expect.any(Array),
message: rule.message ?? null,
description: rule.description ?? null,
});

return oasRules;
}, {}),
Expand All @@ -51,15 +52,17 @@ describe('spectral', () => {
await s.loadRuleset([rulesetName, rulesetName]);

expect(s.rules).toEqual(
Object.entries(expectedRules).reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
oasRules[name] = {
Object.entries(expectedRules).reduce<RunRuleCollection>((oasRules, [name, rule]) => {
oasRules[name] = expect.objectContaining({
name,
...rule,
given: expect.anything(),
formats: expect.arrayContaining([expect.any(String)]),
recommended: expect.any(Boolean),
enabled: expect.any(Boolean),
severity: expect.any(Number),
then: expect.any(Object),
};
then: expect.any(Array),
message: rule.message ?? null,
description: rule.description ?? null,
});

return oasRules;
}, {}),
Expand Down
3 changes: 1 addition & 2 deletions src/cli/services/linter/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from '../../../formats';
import { readParsable } from '../../../fs/reader';
import * as Parsers from '../../../parsers';
import { isRuleEnabled } from '../../../runner';
import { IRuleResult, Spectral } from '../../../spectral';
import { FormatLookup } from '../../../types';
import { ILintConfig } from '../../../types/config';
Expand Down Expand Up @@ -56,7 +55,7 @@ export async function lint(documents: Array<number | string>, flags: ILintConfig
if (flags.verbose) {
if (ruleset) {
const rules = Object.values(spectral.rules);
console.info(`Found ${rules.length} rules (${rules.filter(isRuleEnabled).length} enabled)`);
console.info(`Found ${rules.length} rules (${rules.filter(rule => rule.enabled).length} enabled)`);
} else {
console.info('No rules loaded, attempting to detect document type');
}
Expand Down
2 changes: 1 addition & 1 deletion src/functions/schema-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { JSONPath } from 'jsonpath-plus';

import { Optional } from '@stoplight/types';

import { getLintTargets } from '../runner/utils/getLintTargets';
import { IFunction, IFunctionResult } from '../types';
import { getLintTargets } from '../utils';
import { schema } from './schema';

export interface ISchemaPathOptions {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import * as Parsers from './parsers';

export * from './spectral';
export * from './formats';
export { Rule } from './rule';
export { Parsers };
export { Document, ParsedDocument } from './document';
Loading

0 comments on commit c40094b

Please sign in to comment.