From 09a0dfa19b0715a08b54ed29eafc26b48a32093d Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Tue, 22 Mar 2022 13:13:08 +0100 Subject: [PATCH 1/5] feat(rulesets): add rules for validation of tag names uniqueness --- docs/reference/asyncapi-rules.md | 22 +++ .../asyncapi-tags-uniqueness.test.ts | 140 ++++++++++++++++++ .../__tests__/asyncApi2UniquenessTags.test.ts | 79 ++++++++++ .../functions/asyncApi2DocumentSchema.ts | 2 +- .../functions/asyncApi2UniquenessTags.ts | 33 +++++ packages/rulesets/src/asyncapi/index.ts | 35 +++++ 6 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 4e488bbe2..43666cff4 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -341,6 +341,28 @@ tags: **Recommended:** No +### asyncapi-tags-uniqueness + +Tags have duplicate names (identifiers). + +**Recommended:** Yes + +**Bad Example** + +```yaml +tags: + - name: "Badger" + - name: "Badger" +``` + +**Good Example** + +```yaml +tags: + - name: "Aardvark" + - name: "Badger" +``` + ### asyncapi-tags AsyncAPI object should have non-empty `tags` array. diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts new file mode 100644 index 000000000..6f9e451d1 --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts @@ -0,0 +1,140 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-tags-uniqueness', [ + { + name: 'valid case', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'two' }], + }, + errors: [], + }, + + { + name: 'tags has duplicated names (root)', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'one' }], + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (operation)', + document: { + asyncapi: '2.0.0', + channels: { + someChannel: { + publish: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + subscribe: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'publish', 'tags'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'subscribe', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (operation trait)', + document: { + asyncapi: '2.0.0', + channels: { + someChannel: { + publish: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + subscribe: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Tags contains duplicate tag names: one.', + path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (message)', + document: { + asyncapi: '2.0.0', + components: { + messages: { + someMessage: { + tags: [{ name: 'one' }, { name: 'one' }], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['components', 'messages', 'someMessage', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated names (message trait)', + document: { + asyncapi: '2.0.0', + components: { + messages: { + someMessage: { + traits: [ + { + tags: [{ name: 'one' }, { name: 'one' }], + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Tags contains duplicate tag names: one.', + path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts new file mode 100644 index 000000000..cacf21a19 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts @@ -0,0 +1,79 @@ +import asyncApi2UniquenessTags from '../asyncApi2UniquenessTags'; + +function runValidation(targetVal: Array<{ name: string }>) { + return asyncApi2UniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); +} + +describe('asyncApi2UniquenessTags', () => { + test('should skip empty tags', () => { + const results = runValidation([]); + expect(results).toEqual([]); + }); + + test('should skip valid tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'three', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([]); + }); + + test('should check 1 duplicate tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'one', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([ + { + message: 'Tags contains duplicate tag names: one.', + path: ['tags'], + }, + ]); + }); + + test('should check 2 duplicate tags', () => { + const tags = [ + { + name: 'one', + }, + { + name: 'two', + }, + { + name: 'three', + }, + { + name: 'one', + }, + { + name: 'two', + }, + ]; + + const results = runValidation(tags); + expect(results).toEqual([ + { + message: 'Tags contains duplicate tag names: one, two.', + path: ['tags'], + }, + ]); + }); +}); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts index c32d1fd12..4ac8381d0 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2DocumentSchema.ts @@ -94,7 +94,7 @@ export default createRulesetFunction( input: null, options: null, }, - function oasDocumentSchema(targetVal, _, context) { + function asyncApi2DocumentSchema(targetVal, _, context) { const formats = context.document.formats; if (formats === null || formats === void 0) return; diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts new file mode 100644 index 000000000..a7d6da5f4 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts @@ -0,0 +1,33 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +function getDuplicateTagNames(tags: { name: string }[]) { + const tagNames = tags.map(item => item.name); + return tagNames.reduce((acc, item, idx, arr) => { + if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { + acc.push(item); + } + return acc; + }, [] as string[]); +} + +export default createRulesetFunction, null>( + { + input: null, + options: null, + }, + function asyncApi2UniquenessTags(targetVal, _, ctx) { + if (!targetVal || targetVal.length === 0) return []; + + const duplicatedTags = getDuplicateTagNames(targetVal); + if (!duplicatedTags || duplicatedTags.length === 0) return []; + + return [ + { + message: `Tags contains duplicate tag names: ${duplicatedTags.join(', ')}.`, + path: ctx.path, + }, + ] as IFunctionResult[]; + }, +); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index c36d3ed13..4b9075e9e 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -11,6 +11,7 @@ import { import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; +import asyncApi2UniquenessTags from './functions/asyncApi2UniquenessTags'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -358,6 +359,40 @@ export default { }, }, }, + 'asyncapi-tags-uniqueness': { + description: 'Each tags must have a unique names.', + message: '{{error}}', + severity: 'error', + recommended: true, + type: 'validation', + given: [ + // root + '$.tags', + // operations + '$.channels.*.[publish,subscribe].tags', + '$.components.channels.*.[publish,subscribe].tags', + // operation traits + '$.channels.*.[publish,subscribe].traits.*.tags', + '$.components.channels.*.[publish,subscribe].traits.*.tags', + '$.components.operationTraits.*.tags', + // messages + '$.channels.*.[publish,subscribe].message.tags', + '$.channels.*.[publish,subscribe].message.oneOf.*.tags', + '$.components.channels.*.[publish,subscribe].message.tags', + '$.components.channels.*.[publish,subscribe].message.oneOf.*.tags', + '$.components.messages.*.tags', + // message traits + '$.channels.*.[publish,subscribe].message.traits.*.tags', + '$.channels.*.[publish,subscribe].message.oneOf.*.traits.*.tags', + '$.components.channels.*.[publish,subscribe].message.traits.*.tags', + '$.components.channels.*.[publish,subscribe].message.oneOf.*.traits.*.tags', + '$.components.messages.*.traits.*.tags', + '$.components.messageTraits.*.tags', + ], + then: { + function: asyncApi2UniquenessTags, + }, + }, 'asyncapi-tags': { description: 'AsyncAPI object must have non-empty "tags" array.', recommended: true, From 7f36799aef759ec58e6dd22a8b5dc3e1759d805a Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 7 Apr 2022 14:37:07 +0200 Subject: [PATCH 2/5] feat(rulesets): propagate review suggestions --- .../asyncapi-tags-uniqueness.test.ts | 48 ++++++++++----- .../functions/asyncApi2UniquenessTags.ts | 33 ---------- packages/rulesets/src/asyncapi/index.ts | 4 +- .../__tests__/openapi-tags-uniqueness.test.ts | 48 +++++++++++++++ packages/rulesets/src/oas/index.ts | 12 ++++ .../__tests__/uniquenessTags.test.ts} | 25 +++++--- .../rulesets/src/shared/functions/index.ts | 1 + .../src/shared/functions/uniquenessTags.ts | 61 +++++++++++++++++++ 8 files changed, 176 insertions(+), 56 deletions(-) delete mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts create mode 100644 packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts rename packages/rulesets/src/{asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts => shared/functions/__tests__/uniquenessTags.test.ts} (62%) create mode 100644 packages/rulesets/src/shared/functions/index.ts create mode 100644 packages/rulesets/src/shared/functions/uniquenessTags.ts diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts index 6f9e451d1..2c2f77ea7 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-tags-uniqueness.test.ts @@ -19,8 +19,8 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -43,13 +43,13 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'publish', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'publish', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'subscribe', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'subscribe', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -80,13 +80,13 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'publish', 'traits', '0', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, { - message: 'Tags contains duplicate tag names: one.', - path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['channels', 'someChannel', 'subscribe', 'traits', '0', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -106,8 +106,8 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['components', 'messages', 'someMessage', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['components', 'messages', 'someMessage', 'tags', '1', 'name'], severity: DiagnosticSeverity.Error, }, ], @@ -131,8 +131,28 @@ testRule('asyncapi-tags-uniqueness', [ }, errors: [ { - message: 'Tags contains duplicate tag names: one.', - path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['components', 'messages', 'someMessage', 'traits', '0', 'tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated more that two times this same name', + document: { + asyncapi: '2.0.0', + tags: [{ name: 'one' }, { name: 'one' }, { name: 'two' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '3', 'name'], severity: DiagnosticSeverity.Error, }, ], diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts deleted file mode 100644 index a7d6da5f4..000000000 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2UniquenessTags.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createRulesetFunction } from '@stoplight/spectral-core'; - -import type { IFunctionResult } from '@stoplight/spectral-core'; - -function getDuplicateTagNames(tags: { name: string }[]) { - const tagNames = tags.map(item => item.name); - return tagNames.reduce((acc, item, idx, arr) => { - if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { - acc.push(item); - } - return acc; - }, [] as string[]); -} - -export default createRulesetFunction, null>( - { - input: null, - options: null, - }, - function asyncApi2UniquenessTags(targetVal, _, ctx) { - if (!targetVal || targetVal.length === 0) return []; - - const duplicatedTags = getDuplicateTagNames(targetVal); - if (!duplicatedTags || duplicatedTags.length === 0) return []; - - return [ - { - message: `Tags contains duplicate tag names: ${duplicatedTags.join(', ')}.`, - path: ctx.path, - }, - ] as IFunctionResult[]; - }, -); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 4b9075e9e..0a74000d3 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -11,7 +11,7 @@ import { import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; -import asyncApi2UniquenessTags from './functions/asyncApi2UniquenessTags'; +import { uniquenessTags } from '../shared/functions'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -390,7 +390,7 @@ export default { '$.components.messageTraits.*.tags', ], then: { - function: asyncApi2UniquenessTags, + function: uniquenessTags, }, }, 'asyncapi-tags': { diff --git a/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts b/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts new file mode 100644 index 000000000..61e735edb --- /dev/null +++ b/packages/rulesets/src/oas/__tests__/openapi-tags-uniqueness.test.ts @@ -0,0 +1,48 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('openapi-tags-uniqueness', [ + { + name: 'valid case', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'two' }], + }, + errors: [], + }, + + { + name: 'tags has duplicated names', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'tags has duplicated more that two times this same name', + document: { + swagger: '2.0', + tags: [{ name: 'one' }, { name: 'one' }, { name: 'two' }, { name: 'one' }], + }, + errors: [ + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '1', 'name'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', '3', 'name'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 8d7e817ff..23e65a683 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -25,6 +25,7 @@ import { oasSchema, oasDiscriminator, } from './functions'; +import { uniquenessTags } from '../shared/functions'; export { ruleset as default }; @@ -212,6 +213,17 @@ const ruleset = { }, }, }, + 'openapi-tags-uniqueness': { + description: 'Each tags must have a unique names.', + message: '{{error}}', + severity: 'error', + recommended: true, + type: 'validation', + given: '$.tags', + then: { + function: uniquenessTags, + }, + }, 'openapi-tags': { description: 'OpenAPI object must have non-empty "tags" array.', recommended: false, diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts b/packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts similarity index 62% rename from packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts rename to packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts index cacf21a19..5abb9a849 100644 --- a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2UniquenessTags.test.ts +++ b/packages/rulesets/src/shared/functions/__tests__/uniquenessTags.test.ts @@ -1,10 +1,10 @@ -import asyncApi2UniquenessTags from '../asyncApi2UniquenessTags'; +import uniquenessTags from '../uniquenessTags'; function runValidation(targetVal: Array<{ name: string }>) { - return asyncApi2UniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); + return uniquenessTags(targetVal, null, { path: ['tags'], documentInventory: {} } as any); } -describe('asyncApi2UniquenessTags', () => { +describe('uniquenessTags', () => { test('should skip empty tags', () => { const results = runValidation([]); expect(results).toEqual([]); @@ -43,8 +43,8 @@ describe('asyncApi2UniquenessTags', () => { const results = runValidation(tags); expect(results).toEqual([ { - message: 'Tags contains duplicate tag names: one.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', 2, 'name'], }, ]); }); @@ -66,13 +66,24 @@ describe('asyncApi2UniquenessTags', () => { { name: 'two', }, + { + name: 'two', + }, ]; const results = runValidation(tags); expect(results).toEqual([ { - message: 'Tags contains duplicate tag names: one, two.', - path: ['tags'], + message: '"tags" object contains duplicate tag name "one".', + path: ['tags', 3, 'name'], + }, + { + message: '"tags" object contains duplicate tag name "two".', + path: ['tags', 4, 'name'], + }, + { + message: '"tags" object contains duplicate tag name "two".', + path: ['tags', 5, 'name'], }, ]); }); diff --git a/packages/rulesets/src/shared/functions/index.ts b/packages/rulesets/src/shared/functions/index.ts new file mode 100644 index 000000000..0d2f1ca8a --- /dev/null +++ b/packages/rulesets/src/shared/functions/index.ts @@ -0,0 +1 @@ +export { default as uniquenessTags } from './uniquenessTags'; diff --git a/packages/rulesets/src/shared/functions/uniquenessTags.ts b/packages/rulesets/src/shared/functions/uniquenessTags.ts new file mode 100644 index 000000000..e478bfb17 --- /dev/null +++ b/packages/rulesets/src/shared/functions/uniquenessTags.ts @@ -0,0 +1,61 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +type Tags = Array<{ name: string }>; + +function getDuplicateTagNames(tags: Tags): string[] { + const tagNames = tags.map(item => item.name); + return tagNames.reduce((acc, item, idx, arr) => { + if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { + acc.push(item); + } + return acc; + }, [] as string[]); +} + +export default createRulesetFunction( + { + input: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + }, + options: null, + }, + function uniquenessTags(targetVal, _, ctx) { + const duplicatedTags = getDuplicateTagNames(targetVal); + if (duplicatedTags.length === 0) return []; + + const results: IFunctionResult[] = []; + + duplicatedTags.map(duplicatedTag => { + let checkedFirst = false; + const duplicatedTags: number[] = []; + targetVal.forEach((tag, index) => { + if (tag.name === duplicatedTag) { + if (!checkedFirst) { + checkedFirst = true; + return; + } + duplicatedTags.push(index); + } + }); + + results.push( + ...duplicatedTags.map(duplicatedIndex => ({ + message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, + path: [...ctx.path, duplicatedIndex, 'name'], + })), + ); + }); + + return results; + }, +); From 7765b2231b8a563a5998ad1c82bdf03769e8a201 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 7 Apr 2022 14:43:24 +0200 Subject: [PATCH 3/5] feat(rulesets): add docs for new openapi rule --- docs/reference/openapi-rules.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 2e4e17f3b..69e1884c8 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -250,6 +250,28 @@ tags: - name: "Badger" ``` +### asyncapi-tags-uniqueness + +OpenAPI object should have non-duplicated tag names (identifiers). + +**Recommended:** Yes + +**Bad Example** + +```yaml +tags: + - name: "Badger" + - name: "Badger" +``` + +**Good Example** + +```yaml +tags: + - name: "Aardvark" + - name: "Badger" +``` + ### openapi-tags OpenAPI object should have non-empty `tags` array. From f070f4e6708b2bd9a50614497fcfb65759f58492 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 12:36:11 +0200 Subject: [PATCH 4/5] feat(rulesets): apply review suggestions --- docs/reference/asyncapi-rules.md | 2 +- docs/reference/openapi-rules.md | 4 +- packages/rulesets/src/asyncapi/index.ts | 2 +- packages/rulesets/src/oas/index.ts | 2 +- .../src/shared/functions/uniquenessTags.ts | 44 +++++++------------ 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 43666cff4..fa11723df 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -343,7 +343,7 @@ tags: ### asyncapi-tags-uniqueness -Tags have duplicate names (identifiers). +Tags must not have duplicate names (identifiers). **Recommended:** Yes diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 69e1884c8..3abcbcb47 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -250,9 +250,9 @@ tags: - name: "Badger" ``` -### asyncapi-tags-uniqueness +### openapi-tags-uniqueness -OpenAPI object should have non-duplicated tag names (identifiers). +OpenAPI object must not have duplicated tag names (identifiers). **Recommended:** Yes diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 0a74000d3..660c1bb38 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -360,7 +360,7 @@ export default { }, }, 'asyncapi-tags-uniqueness': { - description: 'Each tags must have a unique names.', + description: 'Each tag must have a unique name.', message: '{{error}}', severity: 'error', recommended: true, diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 23e65a683..9f5c21e74 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -214,7 +214,7 @@ const ruleset = { }, }, 'openapi-tags-uniqueness': { - description: 'Each tags must have a unique names.', + description: 'Each tag must have a unique name.', message: '{{error}}', severity: 'error', recommended: true, diff --git a/packages/rulesets/src/shared/functions/uniquenessTags.ts b/packages/rulesets/src/shared/functions/uniquenessTags.ts index e478bfb17..ba711db2c 100644 --- a/packages/rulesets/src/shared/functions/uniquenessTags.ts +++ b/packages/rulesets/src/shared/functions/uniquenessTags.ts @@ -4,14 +4,15 @@ import type { IFunctionResult } from '@stoplight/spectral-core'; type Tags = Array<{ name: string }>; -function getDuplicateTagNames(tags: Tags): string[] { - const tagNames = tags.map(item => item.name); - return tagNames.reduce((acc, item, idx, arr) => { - if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) { - acc.push(item); - } - return acc; - }, [] as string[]); +function getDuplicateTagsIndexes(tags: Tags): number[] { + return tags + .map(item => item.name) + .reduce((acc, item, i, arr) => { + if (arr.indexOf(item) !== i) { + acc.push(i); + } + return acc; + }, []); } export default createRulesetFunction( @@ -30,31 +31,18 @@ export default createRulesetFunction( options: null, }, function uniquenessTags(targetVal, _, ctx) { - const duplicatedTags = getDuplicateTagNames(targetVal); + const duplicatedTags = getDuplicateTagsIndexes(targetVal); if (duplicatedTags.length === 0) return []; const results: IFunctionResult[] = []; - duplicatedTags.map(duplicatedTag => { - let checkedFirst = false; - const duplicatedTags: number[] = []; - targetVal.forEach((tag, index) => { - if (tag.name === duplicatedTag) { - if (!checkedFirst) { - checkedFirst = true; - return; - } - duplicatedTags.push(index); - } + for (const duplicatedIndex of duplicatedTags) { + const duplicatedTag = targetVal[duplicatedIndex].name; + results.push({ + message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, + path: [...ctx.path, duplicatedIndex, 'name'], }); - - results.push( - ...duplicatedTags.map(duplicatedIndex => ({ - message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, - path: [...ctx.path, duplicatedIndex, 'name'], - })), - ); - }); + } return results; }, From 891d8748278d9d56611abf45693afde7b8115714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 30 May 2022 14:01:27 +0200 Subject: [PATCH 5/5] chore(rulesets): name should be required --- packages/rulesets/src/shared/functions/uniquenessTags.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rulesets/src/shared/functions/uniquenessTags.ts b/packages/rulesets/src/shared/functions/uniquenessTags.ts index ba711db2c..63ebe5c11 100644 --- a/packages/rulesets/src/shared/functions/uniquenessTags.ts +++ b/packages/rulesets/src/shared/functions/uniquenessTags.ts @@ -26,6 +26,7 @@ export default createRulesetFunction( type: 'string', }, }, + required: ['name'], }, }, options: null,