Skip to content

Commit

Permalink
Merge pull request #704 from mkistler/tag-defined
Browse files Browse the repository at this point in the history
Add rule to detect operation tags that are not defined in global tags object
  • Loading branch information
Phil Sturgeon authored Oct 24, 2019
2 parents 648100e + f5eb93c commit 846e8d1
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 9 deletions.
8 changes: 7 additions & 1 deletion docs/reference/openapi-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ Operation should have non-empty `tags` array.

**Recommended:** Yes

### operation-tag-defined

Operation tags should be defined in global tags.

**Recommended:** Yes

### path-declarations-must-exist

Path parameter declarations cannot be empty, ex.`/given/{}` is invalid.
Expand Down Expand Up @@ -568,4 +574,4 @@ Validate structure of OpenAPI v3 specification.

Parameter objects should have a `description`.

**Recommended:** No
**Recommended:** No
9 changes: 9 additions & 0 deletions src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ responses:: !!foo
expect.objectContaining({
code: 'invalid-ref',
}),
expect.objectContaining({
code: 'operation-tag-defined',
}),
expect.objectContaining({
code: 'valid-example-in-schemas',
message: '"foo.example" property type should be number',
Expand Down Expand Up @@ -611,6 +614,12 @@ responses:: !!foo

test('should support YAML merge keys', async () => {
await spectral.loadRuleset('spectral:oas3');
spectral.setRules({
'operation-tag-defined': {
...spectral.rules['operation-tag-defined'],
severity: 'off',
},
});

const result = await spectral.run(petstoreMergeKeys);

Expand Down
153 changes: 153 additions & 0 deletions src/rulesets/oas/functions/__tests__/oasTagDefined.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { RuleType, Spectral } from '../../../../index';

import { DiagnosticSeverity } from '@stoplight/types';
import { rules } from '../../index.json';
import oasTagDefined from '../oasTagDefined';

describe('oasTagDefined', () => {
const s = new Spectral();

s.setFunctions({ oasTagDefined });
s.setRules({
'operation-tag-defined': Object.assign(rules['operation-tag-defined'], {
recommended: true,
type: RuleType[rules['operation-tag-defined'].type],
}),
});

test('validate a correct object', async () => {
const results = await s.run({
tags: [
{
name: 'tag1',
},
{
name: 'tag2',
},
],
paths: {
'/path1': {
get: {
tags: ['tag1'],
},
},
'/path2': {
get: {
tags: ['tag2'],
},
},
},
});
expect(results.length).toEqual(0);
});

test('return errors on undefined tag', async () => {
const results = await s.run({
tags: [
{
name: 'tag1',
},
],
paths: {
'/path1': {
get: {
tags: ['tag2'],
},
},
},
});

expect(results).toEqual([
{
code: 'operation-tag-defined',
message: 'Operation tags should be defined in global tags.',
path: ['paths', '/path1', 'get', 'tags', '0'],
range: {
end: {
character: 16,
line: 10,
},
start: {
character: 10,
line: 10,
},
},
severity: DiagnosticSeverity.Warning,
},
]);
});

test('return errors on undefined tags among defined tags', async () => {
const results = await s.run({
tags: [
{
name: 'tag1',
},
{
name: 'tag3',
},
],
paths: {
'/path1': {
get: {
tags: ['tag1', 'tag2', 'tag3', 'tag4'],
},
},
},
});

expect(results).toEqual([
{
code: 'operation-tag-defined',
message: 'Operation tags should be defined in global tags.',
path: ['paths', '/path1', 'get', 'tags', '1'],
range: {
end: {
character: 16,
line: 14,
},
start: {
character: 10,
line: 14,
},
},
severity: DiagnosticSeverity.Warning,
},
{
code: 'operation-tag-defined',
message: 'Operation tags should be defined in global tags.',
path: ['paths', '/path1', 'get', 'tags', '3'],
range: {
end: {
character: 16,
line: 16,
},
start: {
character: 10,
line: 16,
},
},
severity: DiagnosticSeverity.Warning,
},
]);
});

test('resilient to no global tags or operation tags', async () => {
const results = await s.run({
paths: {
'/path1': {
get: {
operationId: 'id1',
},
},
'/path2': {
get: {
operationId: 'id2',
},
},
},
});

expect(results.length).toEqual(0);
});
});
36 changes: 36 additions & 0 deletions src/rulesets/oas/functions/oasTagDefined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This function will check an API doc to verify that any tag that appears on
// an operation is also present in the global tags array.

import { IFunction, IFunctionResult, Rule } from '../../../types';

export const oasTagDefined: IFunction<Rule> = (targetVal, _options, functionPaths) => {
const results: IFunctionResult[] = [];

const globalTags = (targetVal.tags || []).map(({ name }: { name: string }) => name);

const { paths = {} } = targetVal;

const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];

for (const path in paths) {
if (Object.keys(paths[path]).length > 0) {
for (const operation in paths[path]) {
if (validOperationKeys.indexOf(operation) > -1) {
const { tags = [] } = paths[path][operation];
tags.forEach((tag: string, index: number) => {
if (globalTags.indexOf(tag) === -1) {
results.push({
message: 'Operation tags should be defined in global tags.',
path: ['paths', path, operation, 'tags', index],
});
}
});
}
}
}
}

return results;
};

export default oasTagDefined;
13 changes: 13 additions & 0 deletions src/rulesets/oas/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"oasOpParams",
"oasOpSecurityDefined",
"oasPathParam",
"oasTagDefined",
"refSiblings"
],
"rules": {
Expand Down Expand Up @@ -61,6 +62,18 @@
"operation"
]
},
"operation-tag-defined": {
"description": "Operation tags should be defined in global tags.",
"recommended": true,
"type": "validation",
"given": "$",
"then": {
"function": "oasTagDefined"
},
"tags": [
"operation"
]
},
"path-params": {
"description": "Path parameters should be defined and valid.",
"message": "{{error}}",
Expand Down
1 change: 1 addition & 0 deletions src/rulesets/oas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const commonOasFunctions = (): FunctionCollection => {
oasOpIdUnique: require('./functions/oasOpIdUnique').oasOpIdUnique,
oasOpFormDataConsumeCheck: require('./functions/oasOpFormDataConsumeCheck').oasOpFormDataConsumeCheck,
oasOpParams: require('./functions/oasOpParams').oasOpParams,
oasTagDefined: require('./functions/oasTagDefined').oasTagDefined,
refSiblings: require('./functions/refSiblings').refSiblings,
};
};
Expand Down
2 changes: 1 addition & 1 deletion test-harness/scenarios/enabled-rules-amount.oas3.scenario
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ components:
====command====
lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml -v
====stdout====
Found 55 rules (1 enabled)
Found 56 rules (1 enabled)
Linting {document}
OpenAPI 3.x detected

Expand Down
13 changes: 7 additions & 6 deletions test-harness/scenarios/severity/display-warnings.oas3.scenario
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
====test====
Fail severity is set to error but only warnings exist,
Fail severity is set to error but only warnings exist,
so status should be success and output should show warnings
====document====
openapi: '3.0.0'
Expand Down Expand Up @@ -47,9 +47,10 @@ lint {document} --fail-severity=error
OpenAPI 3.x detected

{document}
1:1 warning api-servers OpenAPI `servers` must be present and non-empty array.
2:6 warning info-contact Info object should contain `contact` object.
2:6 warning info-description OpenAPI object info `description` must be present and non-empty string.
9:9 warning operation-description Operation `description` must be present and non-empty string.
1:1 warning api-servers OpenAPI `servers` must be present and non-empty array.
2:6 warning info-contact Info object should contain `contact` object.
2:6 warning info-description OpenAPI object info `description` must be present and non-empty string.
9:9 warning operation-description Operation `description` must be present and non-empty string.
13:11 warning operation-tag-defined Operation tags should be defined in global tags.

4 problems (0 errors, 4 warnings, 0 infos, 0 hints)
5 problems (0 errors, 5 warnings, 0 infos, 0 hints)
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ OpenAPI 3.x detected
3:10 warning info-description OpenAPI object info `description` must be present and non-empty string.
5:14 hint info-matches-stoplight Info must contain Stoplight
12:13 information operation-description Operation `description` must be present and non-empty string.
15:18 warning operation-tag-defined Operation tags should be defined in global tags.
42:27 error invalid-ref '#/components/schemas/Pets' does not exist
52:27 error invalid-ref '#/components/schemas/Error' does not exist
59:14 information operation-description Operation `description` must be present and non-empty string.
62:18 warning operation-tag-defined Operation tags should be defined in global tags.

8 problems (3 errors, 2 warnings, 2 infos, 1 hint)
10 problems (3 errors, 4 warnings, 2 infos, 1 hint)

0 comments on commit 846e8d1

Please sign in to comment.